mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-06 21:42:49 +00:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da056b75d0 | ||
|
|
51c157d577 | ||
|
|
9f744a3c88 | ||
|
|
d6823a1206 | ||
|
|
beb57b2f5a | ||
|
|
0bda134bfe | ||
|
|
dbcde48c14 | ||
|
|
388e4fbb76 | ||
|
|
9463130e7d | ||
|
|
3a3cf835a5 | ||
|
|
0a10ef1362 | ||
|
|
546aa14895 | ||
|
|
4955d8b6be | ||
|
|
08c2a6c8ab | ||
|
|
dbea099d92 | ||
|
|
4ead266a07 | ||
|
|
6f377264e9 | ||
|
|
a599c87384 | ||
|
|
9566f7aa0a | ||
|
|
af965bce7a | ||
|
|
d61be19a18 | ||
|
|
42384f0ed0 | ||
|
|
0219d0571b | ||
|
|
8043752146 | ||
|
|
29cd22f2d1 | ||
|
|
5abdd9f98f | ||
|
|
327e49ec88 | ||
|
|
13857cd66e | ||
|
|
9a939e9ccd | ||
|
|
dfc0a3aed9 | ||
|
|
2cd2513f64 | ||
|
|
9067d7d6cf | ||
|
|
f0c54777eb | ||
|
|
c8ceefda80 | ||
|
|
2822aac652 | ||
|
|
92a21052f1 | ||
|
|
8013d73fe4 | ||
|
|
b3e107528b | ||
|
|
9a93ea78c3 | ||
|
|
1913e89366 | ||
|
|
8b8088545b | ||
|
|
9f9167c9c7 | ||
|
|
d30de896f0 | ||
|
|
e0ee3258e4 | ||
|
|
e5c9d06ef8 | ||
|
|
fa2b118545 | ||
|
|
99a231800c | ||
|
|
393c50a685 | ||
|
|
3fc0bb0a1f | ||
|
|
b98f856b89 | ||
|
|
c570d39637 | ||
|
|
5815ad8ffd | ||
|
|
4ae968ff0f | ||
|
|
e18858fc58 | ||
|
|
3196cd986e | ||
|
|
5456d4e7c0 | ||
|
|
d364df38de | ||
|
|
d45c4b7154 | ||
|
|
466836cfd0 | ||
|
|
dc79dc8feb | ||
|
|
91de58b5ae | ||
|
|
86709a3465 | ||
|
|
13b4d34341 | ||
|
|
a0731bc26d | ||
|
|
238ceaaaa9 | ||
|
|
52c2ee5d67 | ||
|
|
3baeff23ca | ||
|
|
c2848fc7a5 | ||
|
|
e32da822e1 | ||
|
|
cc6e29619d | ||
|
|
963b0f897c | ||
|
|
4f47409b54 | ||
|
|
a255a50a19 | ||
|
|
28500f64be | ||
|
|
69b3395a1c | ||
|
|
312890f7d8 | ||
|
|
6b92007e3e | ||
|
|
3a5062d9a9 | ||
|
|
ded221c175 | ||
|
|
24f28346bf | ||
|
|
698fd6fa8f | ||
|
|
32eb493f13 | ||
|
|
f95fd09112 | ||
|
|
1958f7f0b4 | ||
|
|
3220739ae4 | ||
|
|
6f3b122231 | ||
|
|
e5659c1aa6 | ||
|
|
d50addf563 | ||
|
|
fd0bab6130 | ||
|
|
337ed38d9c | ||
|
|
c1aa5b4661 | ||
|
|
6fb58f3790 | ||
|
|
45755000f3 | ||
|
|
38f5445634 | ||
|
|
bafcabf7cb | ||
|
|
aadd424400 | ||
|
|
ec6296a606 | ||
|
|
2c43fc09b5 | ||
|
|
b7d092bf94 | ||
|
|
0f056cb650 | ||
|
|
d8cdce680d | ||
|
|
6822e92749 | ||
|
|
3ed681d444 | ||
|
|
e1313b55ff | ||
|
|
f417525195 | ||
|
|
cbc5a57cae | ||
|
|
e7fb36c190 | ||
|
|
075c4e7205 | ||
|
|
9e28ddb313 | ||
|
|
e431098395 | ||
|
|
c1ad32a527 | ||
|
|
89232d9de8 | ||
|
|
de3cbdb60a | ||
|
|
497db2cd07 | ||
|
|
fa66a3abc1 | ||
|
|
e4b97151ae | ||
|
|
1b5dbdb8bc | ||
|
|
385617fe39 | ||
|
|
9cf6fde050 | ||
|
|
a3b16b8a37 | ||
|
|
eed01d6d3d | ||
|
|
184f6dee77 | ||
|
|
923a26ef05 | ||
|
|
7f8c9eda2f | ||
|
|
fba64a4ef2 | ||
|
|
e98c938ba8 | ||
|
|
782fdbebba | ||
|
|
2692263533 | ||
|
|
04fabac8ea | ||
|
|
47e9d0f570 | ||
|
|
1a76c98300 | ||
|
|
c74b7c639b | ||
|
|
1fe63c133d | ||
|
|
33dae87a49 | ||
|
|
9ade1da6db | ||
|
|
8fd916c857 | ||
|
|
dd6a09a4c4 | ||
|
|
cb70ac5354 |
@@ -1,4 +1,3 @@
|
||||
# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
@@ -7,7 +6,6 @@ root = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# We recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -20,7 +20,7 @@ coverage
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
@@ -29,14 +29,14 @@ bower_components
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
@@ -57,6 +57,29 @@ typings/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
#DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
########## end of https://github.com/github/gitignore/blob/master/Node.gitignore
|
||||
|
||||
# ignore ide files
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
109
.gitlab-ci.yml
109
.gitlab-ci.yml
@@ -1,65 +1,114 @@
|
||||
image: registry.gitlab.com/openstapps/projectmanagement/node
|
||||
|
||||
cache:
|
||||
key: ${CI_COMMIT_REF_SLUG}
|
||||
paths:
|
||||
- node_modules
|
||||
before_script:
|
||||
- npm ci
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
- audit
|
||||
- publish
|
||||
- deploy
|
||||
|
||||
build:build:
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- npm install
|
||||
- npm run build
|
||||
artifacts:
|
||||
paths:
|
||||
- lib
|
||||
expire_in: 1 week
|
||||
|
||||
test:audit:
|
||||
stage: test
|
||||
package:
|
||||
dependencies:
|
||||
- build
|
||||
tags:
|
||||
- secrecy
|
||||
stage: publish
|
||||
script:
|
||||
- echo "//registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN" > ~/.npmrc
|
||||
- npm publish
|
||||
only:
|
||||
- /^v[0-9]+.[0-9]+.[0-9]+$/
|
||||
artifacts:
|
||||
paths:
|
||||
- lib
|
||||
|
||||
audit:
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit
|
||||
allow_failure: true
|
||||
except:
|
||||
- schedules
|
||||
|
||||
test:mocha:
|
||||
scheduled-audit:
|
||||
stage: audit
|
||||
script:
|
||||
- npm audit --audit-level=high
|
||||
only:
|
||||
- schedules
|
||||
|
||||
unit:
|
||||
stage: test
|
||||
script:
|
||||
- npm test
|
||||
|
||||
publish:image:
|
||||
image: docker:stable
|
||||
pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- npm run documentation
|
||||
- mv docs public
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- /^v[0-9]+\.[0-9]+\.[0-9]+$/
|
||||
|
||||
docker:
|
||||
image: registry.gitlab.com/openstapps/projectmanagement/builder:latest
|
||||
dependencies:
|
||||
- build:build
|
||||
- test:mocha
|
||||
- test:audit
|
||||
- build
|
||||
stage: publish
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
|
||||
- docker build -t registry.gitlab.com/openstapps/api/copy:$CI_BUILD_REF_NAME .
|
||||
- docker push registry.gitlab.com/openstapps/api/copy:$CI_BUILD_REF_NAME
|
||||
- docker build -t registry.gitlab.com/openstapps/api/copy:latest .
|
||||
- docker push registry.gitlab.com/openstapps/api/copy:latest
|
||||
- export VERSION=$(node -p "require('./package.json').version")
|
||||
- export CORE_VERSION=$(openstapps-projectmanagement get-used-version @openstapps/core)
|
||||
- export IMAGETAG_BASE=$CI_REGISTRY_IMAGE/cli
|
||||
- export IMAGETAG_CORE_VERSION=$IMAGETAG_BASE:core-$CORE_VERSION
|
||||
- export IMAGETAG_VERSION=$IMAGETAG_BASE:$VERSION
|
||||
- export IMAGETAG_LATEST=$IMAGETAG_BASE:latest
|
||||
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
|
||||
- docker build -t $IMAGETAG_LATEST -t $IMAGETAG_VERSION -t $IMAGETAG_CORE_VERSION .
|
||||
- docker push $IMAGETAG_BASE
|
||||
tags:
|
||||
- docker
|
||||
only:
|
||||
- /^v[0-9]+\.[0-9]+\.[0-9]+$/
|
||||
|
||||
deploy:pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- npm run documentation
|
||||
- mv docs public
|
||||
only:
|
||||
- /^v[0-9]+\.[0-9]+\.[0-9]+$/
|
||||
branch:
|
||||
image: registry.gitlab.com/openstapps/projectmanagement/builder:latest
|
||||
stage: publish
|
||||
dependencies:
|
||||
- build
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
untracked: true
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- export IMAGETAG_BASE=$CI_REGISTRY_IMAGE/$REGISTRY_BRANCH/cli
|
||||
- export IMAGETAG_BRANCH=$IMAGETAG_BASE:$CI_COMMIT_REF_SLUG
|
||||
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
|
||||
- docker build -t $IMAGETAG_BRANCH .
|
||||
- docker push $IMAGETAG_BASE
|
||||
except:
|
||||
- /(^v[0-9]+\.[0-9]+\.[0-9]+$|^master$|^develop$)/
|
||||
only:
|
||||
- branches
|
||||
when: manual
|
||||
tags:
|
||||
- docker
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Ignore all files/folders by default
|
||||
# See https://stackoverflow.com/a/29932318
|
||||
/*
|
||||
|
||||
# Execept this files/folders
|
||||
!docs
|
||||
# Except these files/folders
|
||||
!lib
|
||||
lib/tsconfig.tsbuildinfo
|
||||
!LICENSE
|
||||
!package.json
|
||||
!package-lock.json
|
||||
|
||||
193
CHANGELOG.md
193
CHANGELOG.md
@@ -1,3 +1,188 @@
|
||||
# [0.31.0](https://gitlab.com/openstapps/api/compare/v0.30.0...v0.31.0) (2021-08-17)
|
||||
|
||||
|
||||
|
||||
# [0.30.0](https://gitlab.com/openstapps/api/compare/v0.29.0...v0.30.0) (2021-08-04)
|
||||
|
||||
|
||||
|
||||
# [0.29.0](https://gitlab.com/openstapps/api/compare/v0.28.0...v0.29.0) (2021-06-08)
|
||||
|
||||
|
||||
|
||||
# [0.28.0](https://gitlab.com/openstapps/api/compare/v0.27.0...v0.28.0) (2021-04-27)
|
||||
|
||||
|
||||
|
||||
# [0.27.0](https://gitlab.com/openstapps/api/compare/v0.26.0...v0.27.0) (2021-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* send valid JSON on bulk/done ([29cd22f](https://gitlab.com/openstapps/api/commit/29cd22f2d11cebbe2de63cc2e614734e286821f2)), closes [#38](https://gitlab.com/openstapps/api/issues/38)
|
||||
|
||||
|
||||
|
||||
# [0.26.0](https://gitlab.com/openstapps/api/compare/v0.25.0...v0.26.0) (2021-02-23)
|
||||
|
||||
|
||||
|
||||
# [0.25.0](https://gitlab.com/openstapps/api/compare/v0.23.0...v0.25.0) (2020-11-09)
|
||||
|
||||
|
||||
|
||||
# [0.23.0](https://gitlab.com/openstapps/api/compare/v0.22.0...v0.23.0) (2020-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make express parse body ([b3e1075](https://gitlab.com/openstapps/api/commit/b3e107528b5d6ac5ec86786a9f9bf8fb1645e874))
|
||||
|
||||
|
||||
|
||||
# [0.22.0](https://gitlab.com/openstapps/api/compare/v0.21.0...v0.22.0) (2020-05-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add content-type header ([d30de89](https://gitlab.com/openstapps/api/commit/d30de896f0d1a21c039e1905c0766cc37df06b23))
|
||||
|
||||
|
||||
|
||||
# [0.21.0](https://gitlab.com/openstapps/api/compare/v0.19.0...v0.21.0) (2020-05-06)
|
||||
|
||||
|
||||
|
||||
# [0.19.0](https://gitlab.com/openstapps/api/compare/v0.18.0...v0.19.0) (2020-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* repair and unify subcommand help outputs ([4ae968f](https://gitlab.com/openstapps/api/commit/4ae968ff0f52c940cb11a97876f72d57feb61597))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add option to wait on resource to e2e script ([e18858f](https://gitlab.com/openstapps/api/commit/e18858fc58ccfb8b3bdec92260eba12cd1a25214))
|
||||
|
||||
|
||||
|
||||
# [0.18.0](https://gitlab.com/openstapps/api/compare/v0.17.0...v0.18.0) (2020-02-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* extend e2e procedure ([dc79dc8](https://gitlab.com/openstapps/api/commit/dc79dc8feb10a29cff0f6d1c320658ad13cf7e4e))
|
||||
|
||||
|
||||
|
||||
# [0.17.0](https://gitlab.com/openstapps/api/compare/v0.16.0...v0.17.0) (2019-11-15)
|
||||
|
||||
|
||||
|
||||
# [0.16.0](https://gitlab.com/openstapps/api/compare/v0.15.0...v0.16.0) (2019-09-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add plugin class ([c2848fc](https://gitlab.com/openstapps/api/commit/c2848fc7a528288ef4c3ab96728d4e16add6fe75)), closes [#12](https://gitlab.com/openstapps/api/issues/12)
|
||||
|
||||
|
||||
|
||||
# [0.15.0](https://gitlab.com/openstapps/api/compare/v0.14.0...v0.15.0) (2019-08-19)
|
||||
|
||||
|
||||
|
||||
# [0.14.0](https://gitlab.com/openstapps/api/compare/v0.13.0...v0.14.0) (2019-07-23)
|
||||
|
||||
|
||||
|
||||
# [0.13.0](https://gitlab.com/openstapps/api/compare/v0.12.0...v0.13.0) (2019-07-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* change removeReferences() to also remove origin ([ded221c](https://gitlab.com/openstapps/api/commit/ded221c175368d315fdbb79d561c86c3ee8bf158))
|
||||
|
||||
|
||||
|
||||
# [0.12.0](https://gitlab.com/openstapps/api/compare/v0.11.0...v0.12.0) (2019-07-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add function to remove undefined properties ([f95fd09](https://gitlab.com/openstapps/api/commit/f95fd09112e81eab42591a5aaff3568ab06e9441))
|
||||
|
||||
|
||||
|
||||
# [0.11.0](https://gitlab.com/openstapps/api/compare/v0.10.0...v0.11.0) (2019-07-12)
|
||||
|
||||
|
||||
|
||||
# [0.10.0](https://gitlab.com/openstapps/api/compare/v0.9.0...v0.10.0) (2019-07-03)
|
||||
|
||||
|
||||
|
||||
# [0.9.0](https://gitlab.com/openstapps/api/compare/v0.8.0...v0.9.0) (2019-06-20)
|
||||
|
||||
|
||||
|
||||
# [0.8.0](https://gitlab.com/openstapps/api/compare/v0.7.0...v0.8.0) (2019-05-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add e2e command. Indexes core test files. ([e1313b5](https://gitlab.com/openstapps/api/commit/e1313b55ff3bc17e96b09639f4949b2179991679))
|
||||
|
||||
|
||||
|
||||
# [0.7.0](https://gitlab.com/openstapps/api/compare/v0.6.0...v0.7.0) (2019-04-16)
|
||||
|
||||
|
||||
|
||||
# [0.6.0](https://gitlab.com/openstapps/api/compare/v0.5.0...v0.6.0) (2019-04-09)
|
||||
|
||||
|
||||
|
||||
# [0.5.0](https://gitlab.com/openstapps/api/compare/v0.4.1...v0.5.0) (2019-04-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add method to remove references from a thing ([9cf6fde](https://gitlab.com/openstapps/api/commit/9cf6fde050c665b63026b8c08502c7836e23c2d5)), closes [#6](https://gitlab.com/openstapps/api/issues/6)
|
||||
|
||||
|
||||
|
||||
## [0.4.1](https://gitlab.com/openstapps/api/compare/v0.4.0...v0.4.1) (2019-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adjust compile npm script to exact configuration guideline ([7f8c9ed](https://gitlab.com/openstapps/api/commit/7f8c9eda2fdcef0831af23806b846baa0d236a1c))
|
||||
|
||||
|
||||
|
||||
# [0.4.0](https://gitlab.com/openstapps/api/compare/v0.3.0...v0.4.0) (2019-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* apply changes suggested by @openstapps/configuration ([782fdbe](https://gitlab.com/openstapps/api/commit/782fdbebba3599323a98244fcc28e7605ed95cc2))
|
||||
|
||||
|
||||
|
||||
# [0.3.0](https://gitlab.com/openstapps/api/compare/v0.2.0...v0.3.0) (2019-02-21)
|
||||
|
||||
|
||||
|
||||
# [0.2.0](https://gitlab.com/openstapps/api/compare/v0.1.1...v0.2.0) (2019-02-18)
|
||||
|
||||
|
||||
|
||||
## [0.1.1](https://gitlab.com/openstapps/api/compare/v0.1.0...v0.1.1) (2019-02-07)
|
||||
|
||||
|
||||
|
||||
# [0.1.0](https://gitlab.com/openstapps/api/compare/v0.0.3...v0.1.0) (2019-01-28)
|
||||
|
||||
|
||||
@@ -11,17 +196,17 @@
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add type and batchSize to required parameters ([8541b00](https://gitlab.com/openstapps/api/commit/8541b00)), closes [#1](https://gitlab.com/openstapps/api/issues/1)
|
||||
* remove trailing slash from url if needed ([eb7c334](https://gitlab.com/openstapps/api/commit/eb7c334))
|
||||
* add type and batchSize to required parameters ([8541b00](https://gitlab.com/openstapps/api/commit/8541b00dbbf4c683fddaf3d003cf13fabfbc979b)), closes [#1](https://gitlab.com/openstapps/api/issues/1)
|
||||
* remove trailing slash from url if needed ([eb7c334](https://gitlab.com/openstapps/api/commit/eb7c334bb8878cffd96cd9d52b5b99e2338e3a65))
|
||||
|
||||
|
||||
|
||||
## [0.0.1](https://gitlab.com/openstapps/api/compare/4839f94...v0.0.1) (2018-11-29)
|
||||
## [0.0.1](https://gitlab.com/openstapps/api/compare/4839f941c617681c78ef9959f3df86013459c332...v0.0.1) (2018-11-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api ([4839f94](https://gitlab.com/openstapps/api/commit/4839f94))
|
||||
* add api ([4839f94](https://gitlab.com/openstapps/api/commit/4839f941c617681c78ef9959f3df86013459c332))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:lts-alpine
|
||||
FROM registry.gitlab.com/openstapps/projectmanagement/node
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
|
||||
54
README.md
54
README.md
@@ -3,25 +3,65 @@
|
||||
Node.js library to interact with the StApps backend service
|
||||
|
||||
## Use this as a standalone program
|
||||
To get some data into a local `backend-node`-instance, you can run this
|
||||
|
||||
To get some data into a local `backend-node`-instance, you can run this
|
||||
as a standalone program to copy data of a remote `backend-node`-instance
|
||||
into your local one.
|
||||
|
||||
Example to copy all Events of the b-tu instance:
|
||||
Example to copy all Events of the b-tu instance:
|
||||
|
||||
```shell
|
||||
npm install
|
||||
npm run build
|
||||
node ./lib/cli.js copy -t Event --appVersion 1.0.0 https://stappsbe01.innocampus.tu-berlin.de http://localhost:3000
|
||||
node ./lib/cli.js copy Event https://stappsbe01.innocampus.tu-berlin.de http://localhost:3000 100
|
||||
```
|
||||
|
||||
### Program arguments:
|
||||
Example to index all items from @openstapps/core test files to a backend:
|
||||
|
||||
```shell
|
||||
node ./lib/cli.js copy --appVersion <app version> -t <vc type> <source> <target>
|
||||
npm install
|
||||
npm run build
|
||||
node ./lib/cli.js e2e http://localhost:3000
|
||||
```
|
||||
|
||||
Example execution with docker when backend is running on `localhost:3000`:
|
||||
### Program arguments
|
||||
|
||||
```shell
|
||||
docker run --net=host registry.gitlab.com/openstapps/api/copy copy -t Place --appVersion 1.0.0 https://stappsbe01.innocampus.tu-berlin.de http://localhost:3000
|
||||
node ./lib/cli.js copy <type> <from> <to> <batchSize>
|
||||
|
||||
node ./lib/cli.js e2e <to>
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
The source identifier for the bulk to use with the target instance (default is 'copy')
|
||||
|
||||
```shell
|
||||
-s, --bulkSource <bulkScource>
|
||||
```
|
||||
|
||||
The App version to use (unset by default)
|
||||
|
||||
```shell
|
||||
-a, --appVersion <version>
|
||||
```
|
||||
|
||||
The only available option for `e2e` command. File path to json test files each containing a SCThing.
|
||||
|
||||
```shell
|
||||
-s, --samples <path>
|
||||
```
|
||||
|
||||
### Example execution
|
||||
|
||||
with docker when backend is running on `localhost:3000`:
|
||||
|
||||
```shell
|
||||
docker run --net=host registry.gitlab.com/openstapps/api/cli copy Place https://stappsbe01.innocampus.tu-berlin.de http://localhost:3000 100
|
||||
```
|
||||
|
||||
Or using `e2e` command:
|
||||
|
||||
```shell
|
||||
docker run --net=host registry.gitlab.com/openstapps/api/cli e2e http://localhost:3000
|
||||
```
|
||||
6895
package-lock.json
generated
6895
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
100
package.json
100
package.json
@@ -1,61 +1,81 @@
|
||||
{
|
||||
"name": "@openstapps/api",
|
||||
"version": "0.1.1",
|
||||
"version": "0.31.1",
|
||||
"scripts": {
|
||||
"build": "npm run tslint && npm run compile && npm run documentation",
|
||||
"build": "npm run tslint && 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": "rm -rf lib/* && tsc && sed -i '1i#!/usr/bin/env node' lib/cli.js",
|
||||
"documentation": "typedoc --includeDeclarations --excludeExternals --mode modules --out docs src",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "nyc mocha --require ts-node/register --require source-map-support/register --ui mocha-typescript --recursive 'test/*.spec.ts'",
|
||||
"tslint": "tslint 'src/**/*.ts'"
|
||||
"compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'",
|
||||
"documentation": "typedoc --includeDeclarations --mode modules --out docs --readme README.md --listInvalidSymbolLinks src",
|
||||
"postversion": "npm run changelog",
|
||||
"prepublishOnly": "npm ci && npm run build",
|
||||
"preversion": "npm run prepublishOnly",
|
||||
"push": "git push && git push origin \"v$npm_package_version\"",
|
||||
"test": "nyc mocha --require ts-node/register --recursive 'test/*.spec.ts'",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'"
|
||||
},
|
||||
"description": "Node.js library to interact with the StApps backend service",
|
||||
"dependencies": {
|
||||
"@openstapps/core": "0.6.0",
|
||||
"@openstapps/logger": "0.0.5",
|
||||
"@types/cli-progress": "1.8.0",
|
||||
"@types/node": "10.12.21",
|
||||
"@types/request": "2.48.1",
|
||||
"@types/uuid": "3.4.4",
|
||||
"async-pool-native": "0.1.0",
|
||||
"cli-progress": "2.1.1",
|
||||
"commander": "2.19.0",
|
||||
"moment": "2.24.0",
|
||||
"request": "2.88.0",
|
||||
"uuid": "3.3.2"
|
||||
"@krlwlfrt/async-pool": "0.5.0",
|
||||
"@openstapps/core": "0.49.3",
|
||||
"@openstapps/core-tools": "0.24.3",
|
||||
"@openstapps/logger": "0.7.0",
|
||||
"@types/cli-progress": "3.9.2",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/morgan": "1.9.3",
|
||||
"@types/node": "14.17.12",
|
||||
"@types/traverse": "0.6.32",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/wait-on": "5.3.1",
|
||||
"body-parser": "1.19.0",
|
||||
"cli-progress": "3.9.0",
|
||||
"commander": "7.2.0",
|
||||
"express": "4.17.1",
|
||||
"fast-clone": "1.5.13",
|
||||
"got": "11.8.2",
|
||||
"json-schema": "0.3.0",
|
||||
"moment": "2.29.1",
|
||||
"morgan": "1.10.0",
|
||||
"traverse": "0.6.6",
|
||||
"uuid": "8.3.2",
|
||||
"wait-on": "5.3.0"
|
||||
},
|
||||
"license": "GPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@openstapps/configuration": "0.5.1",
|
||||
"@types/chai": "4.1.7",
|
||||
"@types/chai-as-promised": "7.1.0",
|
||||
"@types/chai-spies": "1.0.0",
|
||||
"@types/mocha": "5.2.5",
|
||||
"@types/nock": "9.3.1",
|
||||
"chai": "4.2.0",
|
||||
"@openstapps/configuration": "0.27.0",
|
||||
"@testdeck/mocha": "0.1.2",
|
||||
"@types/chai": "4.2.21",
|
||||
"@types/chai-as-promised": "7.1.4",
|
||||
"@types/chai-spies": "1.0.3",
|
||||
"@types/fs-extra": "9.0.12",
|
||||
"@types/mocha": "8.2.3",
|
||||
"chai": "4.3.4",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"chai-spies": "1.0.0",
|
||||
"conventional-changelog-cli": "2.0.11",
|
||||
"mocha": "5.2.0",
|
||||
"mocha-typescript": "1.1.17",
|
||||
"nock": "10.0.6",
|
||||
"nyc": "13.2.0",
|
||||
"source-map-support": "0.5.10",
|
||||
"ts-node": "8.0.2",
|
||||
"tslint": "5.12.1",
|
||||
"typedoc": "0.14.2",
|
||||
"typescript": "3.3.1"
|
||||
"conventional-changelog-cli": "2.1.1",
|
||||
"fs-extra": "9.1.0",
|
||||
"mocha": "8.4.0",
|
||||
"nock": "13.1.3",
|
||||
"nyc": "15.1.0",
|
||||
"prepend-file-cli": "1.0.6",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-node": "9.1.1",
|
||||
"tslint": "6.1.3",
|
||||
"typedoc": "0.18.0",
|
||||
"typescript": "3.8.3"
|
||||
},
|
||||
"author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
|
||||
"contributors": [
|
||||
"André Michael Thomas Bierlein",
|
||||
"Anselm Stordeur <anselmstordeur@gmail.com>",
|
||||
"Jovan Krunic <jovan.krunic@gmail.com>"
|
||||
"Jovan Krunić <jovan.krunic@gmail.com>",
|
||||
"Michel Jonathan Schmitz",
|
||||
"Rainer Killinger <mail-openstapps@killinger.co>",
|
||||
"Roman Klopsch",
|
||||
"Wieland Schöbl <wulkanat@gmail.com>"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@openstapps/core": "~0.6.0"
|
||||
"@openstapps/core": "~0.49.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -71,7 +91,6 @@
|
||||
"branches": 95,
|
||||
"check-coverage": true,
|
||||
"exclude": [
|
||||
"src/test/**/*.spec.ts",
|
||||
"src/cli.ts"
|
||||
],
|
||||
"extension": [
|
||||
@@ -87,6 +106,9 @@
|
||||
"html",
|
||||
"text-summary"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"statements": 95
|
||||
}
|
||||
}
|
||||
|
||||
10
src/bulk.ts
10
src/bulk.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -46,9 +46,9 @@ export class Bulk<T extends SCThings> {
|
||||
* @see Client.bulk
|
||||
*/
|
||||
constructor(
|
||||
private type: SCThingType,
|
||||
private client: Client,
|
||||
private bulkResponse: SCBulkResponse,
|
||||
private readonly type: SCThingType,
|
||||
private readonly client: Client,
|
||||
private readonly bulkResponse: SCBulkResponse,
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
@@ -78,6 +78,6 @@ export class Bulk<T extends SCThings> {
|
||||
async done(): Promise<SCBulkDoneResponse> {
|
||||
return this.client.invokeRoute<SCBulkDoneResponse>(this.bulkDoneRoute, {
|
||||
UID: this.bulkResponse.uid,
|
||||
});
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
107
src/cli.ts
107
src/cli.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,24 +12,64 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import * as commander from 'commander';
|
||||
import {Command} from 'commander';
|
||||
import {readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {URL} from 'url';
|
||||
import waitOn from 'wait-on';
|
||||
import {copy} from './copy';
|
||||
import {HttpClient} from './httpClient';
|
||||
import {e2eRun} from './e2e';
|
||||
import {HttpClient} from './http-client';
|
||||
|
||||
const logger = new Logger();
|
||||
const pkgJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json')).toString());
|
||||
process.on('unhandledRejection', async (error) => {
|
||||
await Logger.error('unhandledRejection', error);
|
||||
});
|
||||
|
||||
const pkgJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'))
|
||||
.toString());
|
||||
|
||||
const client = new HttpClient();
|
||||
const commander = new Command();
|
||||
const helpAndExit = (help: string) => {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(help);
|
||||
process.exit(-1);
|
||||
};
|
||||
|
||||
let actionDone = false;
|
||||
commander
|
||||
.command('e2e <to>')
|
||||
.version(pkgJson.version)
|
||||
.description('Run in end to end test mode. Indexing and afterwards retrieving all test files from @openstapp/core to the backend')
|
||||
.option('-s --samples [path]', 'Path to @openstapp/core test files', './node_modules/@openstapps/core/test/resources')
|
||||
.option('-w --waiton [resource]', 'wait-on resource parameter see "www.npmjs.com/wait-on"')
|
||||
.action(async (to, e2eCommand) => {
|
||||
let toURL = '';
|
||||
// validate url
|
||||
try {
|
||||
toURL = (new URL(to)).toString();
|
||||
} catch (err) {
|
||||
await Logger.error('expected parameter <to> to be valid url', err);
|
||||
helpAndExit(e2eCommand.helpInformation());
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (error) => {
|
||||
logger.error('unhandledRejection', error);
|
||||
});
|
||||
try {
|
||||
if (typeof e2eCommand.waiton === 'string') {
|
||||
Logger.info(`Waiting for availibilty of resource: ${e2eCommand.waiton}`);
|
||||
await waitOn({
|
||||
resources: [e2eCommand.waiton],
|
||||
timeout: 300000,
|
||||
});
|
||||
Logger.info(`Resource became available`);
|
||||
}
|
||||
await e2eRun(client, {to: toURL, samplesLocation: e2eCommand.samples});
|
||||
Logger.ok('Done');
|
||||
|
||||
} catch (error) {
|
||||
await Logger.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
commander
|
||||
.command('copy <type> <from> <to> <batchSize>')
|
||||
@@ -43,53 +83,50 @@ commander
|
||||
// TODO: remove
|
||||
.option('-a, --appVersion <version>', 'The App version to use [unset by default]')
|
||||
.allowUnknownOption(false)
|
||||
.action((type, from, to, batchSize, copyCommand) => {
|
||||
.action(async (type, from, to, batchSize, copyCommand) => {
|
||||
|
||||
// validate type
|
||||
if (typeof type !== 'string') {
|
||||
logger.error('expected parameter "type" to be of type: string');
|
||||
copyCommand.outputHelp();
|
||||
process.exit(-1);
|
||||
await Logger.error('expected parameter "type" to be of type: string');
|
||||
copyCommand.help();
|
||||
helpAndExit(copyCommand.helpInformation());
|
||||
}
|
||||
|
||||
let fromURL = '';
|
||||
let toURL = '';
|
||||
|
||||
// validate urls
|
||||
try {
|
||||
from = (new URL(from)).toString();
|
||||
to = (new URL(to)).toString();
|
||||
fromURL = (new URL(from)).toString();
|
||||
toURL = (new URL(to)).toString();
|
||||
} catch (err) {
|
||||
logger.error('expected parameters "from" and "to" to be valid urls', err);
|
||||
copyCommand.outputHelp();
|
||||
process.exit(-1);
|
||||
await Logger.error('expected parameters "from" and "to" to be valid urls', err);
|
||||
helpAndExit(copyCommand.helpInformation());
|
||||
}
|
||||
|
||||
// validate batchSize
|
||||
if (isNaN(parseInt(batchSize, 10))) {
|
||||
logger.error('expected parameter "batchSize" to be of type: number');
|
||||
copyCommand.outputHelp();
|
||||
process.exit(-1);
|
||||
await Logger.error('expected parameter "batchSize" to be of type: number');
|
||||
helpAndExit(copyCommand.helpInformation());
|
||||
}
|
||||
|
||||
actionDone = true;
|
||||
|
||||
logger.info('Copying ' + type + ' objects from ' + from + ' to ' + to);
|
||||
Logger.info(`Copying ${type} objects from ${fromURL} to ${toURL}`);
|
||||
|
||||
copy(client, {
|
||||
batchSize: parseInt(batchSize, 10),
|
||||
from: from,
|
||||
from: fromURL,
|
||||
source: copyCommand.bulkSource,
|
||||
to: to,
|
||||
type: type,
|
||||
to: toURL,
|
||||
type: type as SCThingType,
|
||||
version: copyCommand.appVersion,
|
||||
}).then(() => {
|
||||
logger.ok('Done');
|
||||
}, (err) => {
|
||||
throw err;
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
Logger.ok('Done');
|
||||
}, (err) => {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
commander
|
||||
.parse(process.argv);
|
||||
|
||||
if (!actionDone) {
|
||||
commander.outputHelp();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
import {
|
||||
SCAbstractRoute,
|
||||
SCErrorResponse,
|
||||
SCFeedbackRequest,
|
||||
SCFeedbackResponse,
|
||||
SCFeedbackRoute,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
SCThings,
|
||||
} from '@openstapps/core';
|
||||
import {ApiError, CoreVersionIncompatibleError, OutOfRangeError} from './errors';
|
||||
import {HttpClientHeaders, HttpClientInterface} from './httpClientInterface';
|
||||
import {HttpClientHeaders, HttpClientInterface} from './http-client-interface';
|
||||
|
||||
/**
|
||||
* StApps-API client
|
||||
@@ -101,11 +102,12 @@ export class Client {
|
||||
// cut trailing slash if needed
|
||||
this.url = this.url.replace(/\/$/, '');
|
||||
|
||||
this.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (typeof version === 'string') {
|
||||
// set header to tell proxy to select the correct backend
|
||||
this.headers = {
|
||||
'X-StApps-Version': this.version,
|
||||
};
|
||||
this.headers['X-StApps-Version'] = this.version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +117,7 @@ export class Client {
|
||||
* @param feedback Feedback to send
|
||||
*/
|
||||
async feedback(feedback: SCFeedbackRequest): Promise<SCFeedbackResponse> {
|
||||
return await this.invokeRoute<SCFeedbackResponse>(this.feedbackRoute, undefined, feedback);
|
||||
return this.invokeRoute<SCFeedbackResponse>(this.feedbackRoute, undefined, feedback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +169,7 @@ export class Client {
|
||||
* @param body Body for the request
|
||||
*/
|
||||
async invokeRoute<T>(route: SCAbstractRoute,
|
||||
parameters?: { [k: string]: string },
|
||||
parameters?: { [k: string]: string; },
|
||||
body?: SCRequests): Promise<T> {
|
||||
// make the request
|
||||
const response = await this.httpClient.request({
|
||||
@@ -182,7 +184,7 @@ export class Client {
|
||||
return response.body as T;
|
||||
}
|
||||
|
||||
throw new ApiError(response.body);
|
||||
throw new ApiError(response.body as SCErrorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,21 +199,24 @@ export class Client {
|
||||
let preFlightNecessary = false;
|
||||
|
||||
// gather search requests where size is not set
|
||||
Object.keys(multiSearchRequest).forEach((key) => {
|
||||
const searchRequest = multiSearchRequest[key];
|
||||
Object.keys(multiSearchRequest)
|
||||
.forEach((key) => {
|
||||
const searchRequest = multiSearchRequest[key];
|
||||
|
||||
if (typeof searchRequest.size === 'undefined') {
|
||||
preFlightRequest[key] = {
|
||||
...searchRequest,
|
||||
};
|
||||
preFlightRequest[key].size = 0;
|
||||
preFlightNecessary = true;
|
||||
}
|
||||
});
|
||||
if (typeof searchRequest.size === 'undefined') {
|
||||
preFlightRequest[key] = {
|
||||
...searchRequest,
|
||||
};
|
||||
preFlightRequest[key].size = 0;
|
||||
preFlightNecessary = true;
|
||||
}
|
||||
});
|
||||
|
||||
let returnMultiSearchRequest = multiSearchRequest;
|
||||
|
||||
if (preFlightNecessary) {
|
||||
// copy multi search request
|
||||
multiSearchRequest = {
|
||||
returnMultiSearchRequest = {
|
||||
...multiSearchRequest,
|
||||
};
|
||||
|
||||
@@ -223,13 +228,14 @@ export class Client {
|
||||
);
|
||||
|
||||
// set size for multi search requests that were in pre flight request
|
||||
Object.keys(preFlightRequest).forEach((key) => {
|
||||
multiSearchRequest[key].size = preFlightResponse[key].pagination.total;
|
||||
});
|
||||
Object.keys(preFlightRequest)
|
||||
.forEach((key) => {
|
||||
returnMultiSearchRequest[key].size = preFlightResponse[key].pagination.total;
|
||||
});
|
||||
}
|
||||
|
||||
// actually invoke the route
|
||||
return await this.invokeRoute<SCMultiSearchResponse>(this.multiSearchRoute, undefined, multiSearchRequest);
|
||||
return this.invokeRoute<SCMultiSearchResponse>(this.multiSearchRoute, undefined, returnMultiSearchRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,7 +257,7 @@ export class Client {
|
||||
size = preFlightResponse.pagination.total;
|
||||
}
|
||||
|
||||
return await this.invokeRoute<SCSearchResponse>(this.searchRoute, undefined, {
|
||||
return this.invokeRoute<SCSearchResponse>(this.searchRoute, undefined, {
|
||||
...searchRequest,
|
||||
size,
|
||||
});
|
||||
@@ -264,8 +270,10 @@ export class Client {
|
||||
* @param searchResponse Search response for supplied search request
|
||||
*/
|
||||
async searchNext(searchRequest: SCSearchRequest, searchResponse: SCSearchResponse): Promise<{
|
||||
/* tslint:disable:completed-docs */
|
||||
searchRequest: SCSearchRequest;
|
||||
searchResponse: SCSearchResponse;
|
||||
/* tslint:enable:completed-docs */
|
||||
}> {
|
||||
const nextSearchRequest = Client.nextWindow(searchRequest, searchResponse);
|
||||
|
||||
|
||||
255
src/connector-client.ts
Normal file
255
src/connector-client.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool';
|
||||
import {
|
||||
isThing,
|
||||
SCAssociatedThingWithoutReferences,
|
||||
SCBulkResponse,
|
||||
SCBulkRoute,
|
||||
SCLicensePlate,
|
||||
SCNamespaces,
|
||||
SCThings,
|
||||
SCThingType,
|
||||
SCThingUpdateResponse,
|
||||
SCThingUpdateRoute,
|
||||
} from '@openstapps/core';
|
||||
import clone = require('fast-clone');
|
||||
import moment from 'moment';
|
||||
import {v5} from 'uuid';
|
||||
import {Bulk} from './bulk';
|
||||
import {Client} from './client';
|
||||
import {EmptyBulkError, NamespaceNotDefinedError} from './errors';
|
||||
|
||||
/**
|
||||
* StApps-API client
|
||||
*/
|
||||
export class ConnectorClient extends Client {
|
||||
/**
|
||||
* The default timeout for the bulk to expire
|
||||
*/
|
||||
static readonly BULK_TIMEOUT = 3600;
|
||||
/**
|
||||
* The limit of how many items should be indexed concurrently
|
||||
*/
|
||||
static readonly ITEM_CONCURRENT_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* Instance of multi search request route
|
||||
*/
|
||||
private readonly bulkRoute = new SCBulkRoute();
|
||||
|
||||
/**
|
||||
* Instance of multi search request route
|
||||
*/
|
||||
private readonly thingUpdateRoute = new SCThingUpdateRoute();
|
||||
|
||||
/**
|
||||
* Make a UUID from a UID and a namespace ID
|
||||
*
|
||||
* *Note: valid namespace IDs are license plates of StApps universities.
|
||||
* See documentation of `NAMESPACES` for valid namespace IDs.*
|
||||
*
|
||||
* @param uid UID to make UUID from
|
||||
* @param namespaceId Namespace ID to use to make UUID
|
||||
*/
|
||||
static makeUUID(uid: string, namespaceId: SCLicensePlate): string {
|
||||
if (typeof SCNamespaces[namespaceId] === 'undefined') {
|
||||
throw new NamespaceNotDefinedError(namespaceId);
|
||||
}
|
||||
|
||||
return v5(uid.toString(), SCNamespaces[namespaceId]!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove fields from a thing that are references
|
||||
*
|
||||
* This effectively turns a thing into a thing without references, e.g. SCDish into SCDishWithoutReferences.
|
||||
*
|
||||
* @param thing Thing to remove references from
|
||||
*/
|
||||
static removeReferences<THING extends SCThings>(thing: THING): SCAssociatedThingWithoutReferences<THING> {
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
const thingWithoutReferences = clone<any>(thing);
|
||||
|
||||
delete thingWithoutReferences.origin;
|
||||
|
||||
// iterate over all properties
|
||||
for (const key in thingWithoutReferences) {
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (!thingWithoutReferences.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const property = thingWithoutReferences[key];
|
||||
|
||||
// check if property itself is a thing
|
||||
if (isThing(property)) {
|
||||
// delete said property
|
||||
delete thingWithoutReferences[key];
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if property is an array
|
||||
if (Array.isArray(property)) {
|
||||
if (property.every(isThing)) {
|
||||
// delete property if every item in it is a thing
|
||||
delete thingWithoutReferences[key];
|
||||
} else {
|
||||
// check every item in array
|
||||
for (const item of property) {
|
||||
if (['boolean', 'number', 'string'].indexOf(typeof item) >= 0) {
|
||||
// skip primitives
|
||||
continue;
|
||||
}
|
||||
|
||||
// check every property
|
||||
for (const itemKey in item) {
|
||||
/* istanbul ignore if */
|
||||
if (!item.hasOwnProperty(itemKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isThing(item[itemKey])) {
|
||||
// delete properties that are things
|
||||
delete item[itemKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof property === 'object') {
|
||||
// iterate over all properties in nested objects
|
||||
for (const nestedKey in property) {
|
||||
if (isThing(property[nestedKey])) {
|
||||
// delete properties that are things
|
||||
delete property[nestedKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return thingWithoutReferences as SCAssociatedThingWithoutReferences<THING>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively deletes all undefined properties from an object instance
|
||||
*
|
||||
* @param obj Object to delete undefined properties from
|
||||
*/
|
||||
static removeUndefinedProperties(obj: object): void {
|
||||
// return atomic data types and arrays (recursion anchor)
|
||||
if (typeof obj !== 'object' || Array.isArray(obj)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check each key
|
||||
for (const key in obj) {
|
||||
/* istanbul ignore if */
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const indexedObj = obj as { [k: string]: unknown; };
|
||||
|
||||
if (typeof indexedObj[key] === 'undefined') {
|
||||
// delete undefined keyss
|
||||
delete indexedObj[key];
|
||||
} else {
|
||||
// check recursive
|
||||
ConnectorClient.removeUndefinedProperties(indexedObj[key] as object);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a bulk transfer to the backend
|
||||
*
|
||||
* This uses the Bulk API supplied by the backend and returns an object that can be used
|
||||
* just like the client itself, while handling the information necessary in bulk transfers.
|
||||
*
|
||||
* @param type StAppsCore thing type
|
||||
* @param source Source identifier (should be unique per actual data source)
|
||||
* @param timeout Timeout in seconds when the bulk should expire
|
||||
*/
|
||||
async bulk<T extends SCThings>(type: SCThingType, source: string, timeout?: number): Promise<Bulk<T>> {
|
||||
let bulkTimeout: number;
|
||||
// set default value for timeout to one hour
|
||||
if (typeof timeout !== 'number') {
|
||||
bulkTimeout = ConnectorClient.BULK_TIMEOUT;
|
||||
} else {
|
||||
bulkTimeout = timeout;
|
||||
}
|
||||
|
||||
const bulkData = await this.invokeRoute<SCBulkResponse>(this.bulkRoute, undefined, {
|
||||
expiration: moment()
|
||||
.add(bulkTimeout, 'seconds')
|
||||
.format(),
|
||||
source: source,
|
||||
type: type,
|
||||
});
|
||||
|
||||
return new Bulk(type, this, bulkData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index a list of things
|
||||
*
|
||||
* Note that source is optional but is set to `'stapps-api'` in that case.
|
||||
* This will override any previous bulk that you indexed with that source.
|
||||
*
|
||||
* @param things List of things to index
|
||||
* @param source Source of the things
|
||||
* @param timeout Timeout of the bulk in seconds
|
||||
* @see ConnectorClient.bulk
|
||||
*/
|
||||
async index<T extends SCThings>(things: T[], source?: string, timeout?: number): Promise<void> {
|
||||
// check that number of things is not zero
|
||||
if (things.length === 0) {
|
||||
throw new EmptyBulkError();
|
||||
}
|
||||
|
||||
let thingSource: string;
|
||||
// set default source if none is given
|
||||
if (typeof source === 'undefined') {
|
||||
thingSource = 'stapps-api';
|
||||
} else {
|
||||
thingSource = source;
|
||||
}
|
||||
|
||||
// request a new bulk
|
||||
const bulk = await this.bulk(things[0].type, thingSource, timeout);
|
||||
|
||||
// add items to the bulk - 5 concurrently
|
||||
await asyncPool(ConnectorClient.ITEM_CONCURRENT_LIMIT, things, (thing) => bulk.add(thing));
|
||||
|
||||
// close bulk
|
||||
await bulk.done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing StAppsCore thing
|
||||
*
|
||||
* @param thing StAppsCore thing to update
|
||||
*/
|
||||
async update(thing: SCThings): Promise<SCThingUpdateResponse> {
|
||||
return this.invokeRoute<SCThingUpdateResponse>(this.thingUpdateRoute, {
|
||||
TYPE: encodeURIComponent(thing.type),
|
||||
UID: encodeURIComponent(thing.uid),
|
||||
}, thing);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {
|
||||
SCBulkResponse,
|
||||
SCBulkRoute,
|
||||
SCLicensePlate,
|
||||
SCNamespaces,
|
||||
SCThings,
|
||||
SCThingType,
|
||||
SCThingUpdateResponse,
|
||||
SCThingUpdateRoute,
|
||||
} from '@openstapps/core';
|
||||
import {asyncPool} from 'async-pool-native/dist/async-pool';
|
||||
import * as moment from 'moment';
|
||||
import {Bulk} from './bulk';
|
||||
import {Client} from './client';
|
||||
import {EmptyBulkError, NamespaceNotDefinedError} from './errors';
|
||||
|
||||
/* tslint:disable:no-var-requires */
|
||||
|
||||
/**
|
||||
* The package @types/uuid unfortunately doesn't expose the browser versions of the hashing functions.
|
||||
* That's why we need to use a little trickery to get to it.
|
||||
*/
|
||||
const v35 = require('uuid/lib/v35');
|
||||
const sha1Browser = require('uuid/lib/sha1-browser');
|
||||
const v5 = v35('v5', 0x50, sha1Browser);
|
||||
|
||||
/* tslint:enable */
|
||||
|
||||
export class ConnectorClient extends Client {
|
||||
/**
|
||||
* Instance of multi search request route
|
||||
*/
|
||||
private readonly bulkRoute = new SCBulkRoute();
|
||||
|
||||
/**
|
||||
* Instance of multi search request route
|
||||
*/
|
||||
private readonly thingUpdateRoute = new SCThingUpdateRoute();
|
||||
|
||||
/**
|
||||
* Make a UUID from a UID and a namespace ID
|
||||
*
|
||||
* *Note: valid namespace IDs are license plates of StApps universities.
|
||||
* See documentation of `NAMESPACES` for valid namespace IDs.*
|
||||
*
|
||||
* @param uid UID to make UUID from
|
||||
* @param namespaceId Namespace ID to use to make UUID
|
||||
*/
|
||||
static makeUUID(uid: string, namespaceId: SCLicensePlate): string {
|
||||
if (typeof SCNamespaces[namespaceId] === 'undefined') {
|
||||
throw new NamespaceNotDefinedError(namespaceId);
|
||||
}
|
||||
|
||||
return v5(uid.toString(), SCNamespaces[namespaceId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a bulk transfer to the backend
|
||||
*
|
||||
* This uses the Bulk API supplied by the backend and returns an object that can be used
|
||||
* just like the client itself, while handling the information necessary in bulk transfers.
|
||||
*
|
||||
* @param type StAppsCore thing type
|
||||
* @param source Source identifier (should be unique per actual data source)
|
||||
* @param timeout Timeout in seconds when the bulk should expire
|
||||
*/
|
||||
async bulk<T extends SCThings>(type: SCThingType, source: string, timeout?: number): Promise<Bulk<T>> {
|
||||
// set default value for timeout to one hour
|
||||
if (typeof timeout !== 'number') {
|
||||
timeout = 3600;
|
||||
}
|
||||
|
||||
const bulkData = await this.invokeRoute<SCBulkResponse>(this.bulkRoute, undefined, {
|
||||
expiration: moment().add(timeout, 'seconds').format(),
|
||||
source: source,
|
||||
type: type,
|
||||
});
|
||||
|
||||
return new Bulk(type, this, bulkData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index a list of things
|
||||
*
|
||||
* Note that source is optional but is set to `'stapps-api'` in that case.
|
||||
* This will override any previous bulk that you indexed with that source.
|
||||
*
|
||||
* @param things List of things to index
|
||||
* @param source Source of the things
|
||||
* @param timeout Timeout of the bulk in seconds
|
||||
* @see ConnectorClient.bulk
|
||||
*/
|
||||
async index<T extends SCThings>(things: T[], source?: string, timeout?: number): Promise<void> {
|
||||
// check that number of things is not zero
|
||||
if (things.length === 0) {
|
||||
throw new EmptyBulkError();
|
||||
}
|
||||
|
||||
// set default source if none is given
|
||||
if (typeof source === 'undefined') {
|
||||
source = 'stapps-api';
|
||||
}
|
||||
|
||||
// request a new bulk
|
||||
const bulk = await this.bulk(things[0].type, source, timeout);
|
||||
|
||||
// add items to the bulk - 5 concurrently
|
||||
await asyncPool(5, things, (thing) => bulk.add(thing));
|
||||
|
||||
// close bulk
|
||||
await bulk.done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing StAppsCore thing
|
||||
*
|
||||
* @param thing StAppsCore thing to update
|
||||
*/
|
||||
async update(thing: SCThings): Promise<SCThingUpdateResponse> {
|
||||
return this.invokeRoute<SCThingUpdateResponse>(this.thingUpdateRoute, {
|
||||
TYPE: encodeURIComponent(thing.type),
|
||||
UID: encodeURIComponent(thing.uid),
|
||||
}, thing);
|
||||
}
|
||||
}
|
||||
10
src/copy.ts
10
src/copy.ts
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,13 +12,13 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool';
|
||||
import {SCSearchRequest, SCThingType} from '@openstapps/core';
|
||||
import {asyncPool} from 'async-pool-native/dist/async-pool';
|
||||
import {Bar} from 'cli-progress';
|
||||
import {Client} from './client';
|
||||
import {ConnectorClient} from './connectorClient';
|
||||
import {ConnectorClient} from './connector-client';
|
||||
import {OutOfRangeError} from './errors';
|
||||
import {HttpClientInterface} from './httpClientInterface';
|
||||
import {HttpClientInterface} from './http-client-interface';
|
||||
|
||||
/**
|
||||
* Options to set up copying data from one backend to another
|
||||
@@ -93,7 +93,7 @@ export async function copy(client: HttpClientInterface, options: CopyOptions): P
|
||||
try {
|
||||
({searchRequest, searchResponse} = await apiIn.searchNext(searchRequest, searchResponse));
|
||||
|
||||
await asyncPool(5, searchResponse.data, (item) => {
|
||||
await asyncPool(ConnectorClient.ITEM_CONCURRENT_LIMIT, searchResponse.data, async (item) => {
|
||||
progressBar.increment(1);
|
||||
|
||||
return bulk.add(item);
|
||||
|
||||
162
src/e2e.ts
Normal file
162
src/e2e.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {SCSearchRequest, SCThings, SCThingType} from '@openstapps/core';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {deepStrictEqual} from 'assert';
|
||||
import {readdir, readFile} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {promisify} from 'util';
|
||||
import {ConnectorClient} from './connector-client';
|
||||
import {HttpClientInterface} from './http-client-interface';
|
||||
|
||||
const localItemMap: Map<string, SCThings> = new Map();
|
||||
const remoteItemMap: Map<string, SCThings> = new Map();
|
||||
|
||||
/**
|
||||
* Options to set up indexing core test files to backend
|
||||
*/
|
||||
export interface E2EOptions {
|
||||
/**
|
||||
* File path of the directory containing core test files
|
||||
*/
|
||||
samplesLocation: string;
|
||||
|
||||
/**
|
||||
* URL of the backend to index to
|
||||
*/
|
||||
to: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that can be used for integration tests.
|
||||
* Adds all the SCThings that getItemsFromSamples() returns to the backend.
|
||||
* Afterwards retrieves the items from backend and checks for differences with original ones.
|
||||
*/
|
||||
export async function e2eRun(client: HttpClientInterface, options: E2EOptions): Promise<void> {
|
||||
localItemMap.clear();
|
||||
remoteItemMap.clear();
|
||||
|
||||
const api = new ConnectorClient(client, options.to);
|
||||
try {
|
||||
await indexSamples(api, options);
|
||||
Logger.info(`All samples have been indexed via the backend`);
|
||||
|
||||
await retrieveItems(api);
|
||||
Logger.info(`All samples have been retrieved from the backend`);
|
||||
compareItems();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retieves all samples previously index using the api
|
||||
*/
|
||||
async function retrieveItems(api: ConnectorClient): Promise<void> {
|
||||
const singleItemSearchRequest: SCSearchRequest = {
|
||||
filter: {
|
||||
arguments: {
|
||||
field: 'uid',
|
||||
value: 'replace-me',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
};
|
||||
for (const uid of localItemMap.keys()) {
|
||||
singleItemSearchRequest.filter!.arguments.value = uid;
|
||||
const searchResonse = await api.search(singleItemSearchRequest);
|
||||
if (searchResonse.data.length !== 1) {
|
||||
throw Error(`Search for single SCThing with uid: ${uid} returned ${searchResonse.data.length} results`);
|
||||
}
|
||||
remoteItemMap.set(uid, searchResonse.data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares all samples (local and remote) with the same uid and throws if they're not deep equal
|
||||
*/
|
||||
function compareItems() {
|
||||
for (const localThing of localItemMap.values()) {
|
||||
/* istanbul ignore next retrieveItems will throw before*/
|
||||
if (!remoteItemMap.has(localThing.uid)) {
|
||||
throw Error(`Did not retrieve expected SCThing with uid: ${localThing.uid}`);
|
||||
}
|
||||
const remoteThing = remoteItemMap.get(localThing.uid);
|
||||
deepStrictEqual(remoteThing, localThing, `Unexpected difference between original and retrieved sample`);
|
||||
}
|
||||
Logger.info(`All samples retrieved from the backend are the same (deep equal) as the original ones submitted`);
|
||||
}
|
||||
/**
|
||||
* Function to add all the SCThings that getItemsFromSamples() returns to the backend
|
||||
*/
|
||||
async function indexSamples(api: ConnectorClient, options: E2EOptions): Promise<void> {
|
||||
|
||||
const items = await getItemsFromSamples(options.samplesLocation);
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error('Could not index samples. None were retrieved from the file system.');
|
||||
}
|
||||
|
||||
try {
|
||||
// sort items by type
|
||||
const itemMap: Map<SCThingType, SCThings[]> = new Map();
|
||||
for (const item of items) {
|
||||
if (!itemMap.has(item.type)) {
|
||||
itemMap.set(item.type, []);
|
||||
}
|
||||
const itemsOfSameType = itemMap.get(item.type) as SCThings[];
|
||||
itemsOfSameType.push(item);
|
||||
itemMap.set(item.type, itemsOfSameType);
|
||||
localItemMap.set(item.uid, item);
|
||||
}
|
||||
// add items depending on their type property with one type per bulk
|
||||
for (const type of itemMap.keys()) {
|
||||
await api.index(itemMap.get(type) as SCThings[], 'stapps-core-sample-data');
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all SCThings from the predefined core test json files
|
||||
*
|
||||
* @param samplesDirectory Filepath to the directory containing to the core test json files
|
||||
* @returns an Array of all the SCThings specified for test usage
|
||||
*/
|
||||
export async function getItemsFromSamples<T extends SCThings>(samplesDirectory: string): Promise<T[]> {
|
||||
const readDirPromised = promisify(readdir);
|
||||
const readFilePromised = promisify(readFile);
|
||||
|
||||
const things: T[] = [];
|
||||
try {
|
||||
const fileNames = await readDirPromised(samplesDirectory);
|
||||
for (const fileName of fileNames) {
|
||||
const filePath = join(samplesDirectory, fileName);
|
||||
if (filePath.endsWith('.json')) {
|
||||
const fileContent = await readFilePromised(filePath, {encoding: 'utf8'});
|
||||
const schemaObject = JSON.parse(fileContent);
|
||||
if (schemaObject.errorNames.length === 0 && typeof schemaObject.instance.type === 'string') {
|
||||
things.push(schemaObject.instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return things;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -39,12 +39,12 @@ export class ApiError extends Error {
|
||||
|
||||
// add additional data
|
||||
if (typeof this.data.additionalData !== 'undefined') {
|
||||
str += '\n\n' + JSON.stringify(this.data.additionalData);
|
||||
str += `\n\n${JSON.stringify(this.data.additionalData)}`;
|
||||
}
|
||||
|
||||
// add "remote" stack trace
|
||||
if (typeof this.data.stack !== 'undefined') {
|
||||
str += '\n\n' + this.data.stack;
|
||||
str += `\n\n${this.data.stack}`;
|
||||
}
|
||||
|
||||
return str;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -32,7 +32,9 @@ export interface HttpClientInterface {
|
||||
* A map of headers
|
||||
*/
|
||||
export interface HttpClientHeaders {
|
||||
/* tslint:disable:no-any */
|
||||
[key: string]: any;
|
||||
/* tslint:enable:no-any */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +74,9 @@ export interface HttpClientRequest {
|
||||
* A HTTP client response
|
||||
*/
|
||||
export interface HttpClientResponse<T extends SCResponses> {
|
||||
/**
|
||||
* Body of the response
|
||||
*/
|
||||
body: T | SCErrorResponse;
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2019-2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,19 +12,31 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import * as request from 'request';
|
||||
import got, {OptionsOfJSONResponseBody, Response as GotResponse} from 'got';
|
||||
|
||||
/**
|
||||
* Request options that requires a url
|
||||
* Note: adjust request options of got library for backward compatibility
|
||||
*/
|
||||
export interface RequestOptions extends request.CoreOptions {
|
||||
export interface RequestOptions extends Omit<OptionsOfJSONResponseBody, 'json' | 'body'> {
|
||||
/**
|
||||
* Body of the request
|
||||
*/
|
||||
// tslint:disable-next-line:no-any TODO: Use a specific type?
|
||||
body?: any;
|
||||
/**
|
||||
* Target URL of the request
|
||||
*/
|
||||
url: URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response with generic for the type of body that is returned from the request
|
||||
*/
|
||||
export interface Response<TYPE_OF_BODY> extends request.Response {
|
||||
export interface Response<TYPE_OF_BODY> extends GotResponse {
|
||||
/**
|
||||
* Typed body of the response
|
||||
*/
|
||||
body: TYPE_OF_BODY;
|
||||
}
|
||||
|
||||
@@ -36,36 +48,38 @@ export class HttpClient {
|
||||
* Make a request
|
||||
* @param requestConfig Configuration of the request
|
||||
*/
|
||||
request<TYPE_OF_BODY>(
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
async request<TYPE_OF_BODY>(
|
||||
requestConfig: RequestOptions,
|
||||
): Promise<Response<TYPE_OF_BODY>> {
|
||||
const params: request.CoreOptions = {
|
||||
body: {},
|
||||
followAllRedirects: true,
|
||||
json: true,
|
||||
const params: OptionsOfJSONResponseBody = {
|
||||
followRedirect: true,
|
||||
method: 'GET',
|
||||
responseType: 'json',
|
||||
};
|
||||
|
||||
if (typeof requestConfig.body !== 'undefined') {
|
||||
params.body = requestConfig.body;
|
||||
params.json = requestConfig.body;
|
||||
}
|
||||
|
||||
if (typeof requestConfig.headers !== 'undefined') {
|
||||
params.headers = requestConfig.headers;
|
||||
}
|
||||
|
||||
if (requestConfig.method !== 'GET') {
|
||||
if (typeof requestConfig.method !== 'undefined') {
|
||||
params.method = requestConfig.method;
|
||||
}
|
||||
let response: Response<TYPE_OF_BODY>;
|
||||
try {
|
||||
response = await got(requestConfig.url.toString(), params);
|
||||
} catch (err) {
|
||||
if (typeof err.response === 'undefined') {
|
||||
throw err;
|
||||
}
|
||||
// if there is a response (e.g. response with statusCode 404 etc.) provide it
|
||||
response = err.response;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(requestConfig.url.toString(), params, (err, response) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
return response;
|
||||
}
|
||||
}
|
||||
70
src/plugin-client.ts
Normal file
70
src/plugin-client.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {SCPluginRegisterRequest, SCPluginRegisterRoute} from '@openstapps/core';
|
||||
import {ConnectorClient} from './connector-client';
|
||||
import {Plugin} from './plugin';
|
||||
|
||||
/**
|
||||
* The PluginClient for registering and unregistering HTTP Plugins
|
||||
*
|
||||
* It contains a lot of the boilerplate for creating plugins, and thus simplifies the creation of such.
|
||||
*/
|
||||
export class PluginClient extends ConnectorClient {
|
||||
/**
|
||||
* Register a plugin in the backend
|
||||
*
|
||||
* **This method automatically calls [[Plugin.start]]**
|
||||
* You need to call this method before you can do anything with the plugin. If you want to register the plugin again,
|
||||
* you might first want to inform yourself how the backend behaves in such cases TODO: add docs for this
|
||||
*
|
||||
* @param plugin The instance of the plugin you want to register
|
||||
*/
|
||||
async registerPlugin(plugin: Plugin) {
|
||||
const request: SCPluginRegisterRequest = {
|
||||
action: 'add',
|
||||
plugin: {
|
||||
address: plugin.fullUrl,
|
||||
name: plugin.name,
|
||||
requestSchema: plugin.requestSchema,
|
||||
responseSchema: plugin.responseSchema,
|
||||
route: plugin.route,
|
||||
},
|
||||
};
|
||||
await this.invokeRoute(new SCPluginRegisterRoute(), undefined, request);
|
||||
|
||||
// start the plugin we just registered
|
||||
plugin.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a plugin from the backend
|
||||
*
|
||||
* **This method automatically calls [[Plugin.stop]]**
|
||||
* If you want to unregister your plugin for some reason, you can do so by calling this method.
|
||||
* *Use with caution.*
|
||||
*
|
||||
* @param plugin The instance of the plugin you want to register
|
||||
*/
|
||||
async unregisterPlugin(plugin: Plugin) {
|
||||
const request: SCPluginRegisterRequest = {
|
||||
action: 'remove',
|
||||
route: plugin.route,
|
||||
};
|
||||
// stop the plugin we want to unregister
|
||||
plugin.stop();
|
||||
|
||||
await this.invokeRoute(new SCPluginRegisterRoute(), undefined, request);
|
||||
}
|
||||
}
|
||||
252
src/plugin.ts
Normal file
252
src/plugin.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Converter} from '@openstapps/core-tools/lib/schema';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import bodyParser from 'body-parser';
|
||||
import express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as http2 from 'http2';
|
||||
import {JSONSchema7} from 'json-schema';
|
||||
import morgan from 'morgan';
|
||||
import ErrnoException = NodeJS.ErrnoException;
|
||||
|
||||
/**
|
||||
* The Plugin for creating HTTP backend plugins
|
||||
*
|
||||
* It contains a lot of the boilerplate for creating plugins, and thus simplifies the creation of such.
|
||||
* To create your own plugin, you need to extend this class and implement the [[Plugin.onRouteInvoke]] method
|
||||
*/
|
||||
export abstract class Plugin {
|
||||
/**
|
||||
* The express instance
|
||||
*/
|
||||
private readonly app = express();
|
||||
|
||||
/**
|
||||
* The HTTP server
|
||||
*/
|
||||
private readonly server: http.Server;
|
||||
|
||||
/**
|
||||
* Whether the server is active or not
|
||||
*
|
||||
* When active is false, it will return 404 on all routes.
|
||||
*/
|
||||
protected active = false;
|
||||
|
||||
/**
|
||||
* The full URL of the plugin
|
||||
*
|
||||
* The full URL of the plugin consists out of URL:PORT
|
||||
*/
|
||||
public get fullUrl() {
|
||||
return `${this.url}:${this.port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The port on which the plugin will listen on
|
||||
*/
|
||||
public port: string | number | false;
|
||||
/**
|
||||
* The schema of the request interfaces defined by the user
|
||||
*/
|
||||
public readonly requestSchema: JSONSchema7 = {};
|
||||
/**
|
||||
* The schema of the response interfaces defined by the user
|
||||
*/
|
||||
public readonly responseSchema: JSONSchema7 = {};
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*
|
||||
* @param value the port you want to normalize
|
||||
*/
|
||||
protected static normalizePort(value: string) {
|
||||
const portNumber = parseInt(value, 10);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (isNaN(portNumber)) {
|
||||
// named pipe
|
||||
/* istanbul ignore next */
|
||||
return value;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (portNumber >= 0) {
|
||||
// port number
|
||||
return portNumber;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the PluginClient
|
||||
*
|
||||
* Don't forget to call [[PluginClient.registerPlugin]]!
|
||||
* Refer to the examples for how to use the schemas. TODO: examples
|
||||
*
|
||||
* @param port The port of the plugin
|
||||
* @param name The name of the plugin
|
||||
* @param url The url of the plugin without the port or anything else, for example `http://localhost`
|
||||
* @param route The desired route that will be registered in the backend
|
||||
* @param backendUrl The url of the backend
|
||||
* @param converter If you want to use an already existing converter, you can pass it here
|
||||
* @param requestName the name of the request schema
|
||||
* @param responseName the name of the response schema
|
||||
* @param version the version. You should retrieve it from the package.json
|
||||
*/
|
||||
constructor(port: number,
|
||||
public name: string,
|
||||
public url: string,
|
||||
public route: string,
|
||||
protected backendUrl: string,
|
||||
converter: Converter,
|
||||
requestName: string,
|
||||
responseName: string,
|
||||
version: string) {
|
||||
|
||||
this.app.use(bodyParser.json());
|
||||
this.port = Plugin.normalizePort(
|
||||
/* istanbul ignore next */
|
||||
typeof process.env.PORT !== 'undefined' ? process.env.PORT : port.toString());
|
||||
this.app.set('port', this.port);
|
||||
|
||||
// setup express
|
||||
this.server = http.createServer(this.app);
|
||||
this.server.listen(this.port);
|
||||
/* istanbul ignore next */
|
||||
this.server.on('error', (err) => {
|
||||
/* istanbul ignore next */
|
||||
this.onError(err);
|
||||
});
|
||||
this.server.on('listening', () => {
|
||||
this.onListening();
|
||||
});
|
||||
|
||||
this.requestSchema = converter.getSchema(requestName, version);
|
||||
this.responseSchema = converter.getSchema(responseName, version);
|
||||
|
||||
this.app.use(morgan('dev'));
|
||||
|
||||
this.app.set('env', process.env.NODE_ENV);
|
||||
|
||||
this.app.all('*', async (req: express.Request, res: express.Response) => {
|
||||
if (this.active) {
|
||||
await this.onRouteInvoke(req, res);
|
||||
} else {
|
||||
res.status(http2.constants.HTTP_STATUS_NOT_FOUND);
|
||||
res.send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*
|
||||
* @param error The error that occurred
|
||||
*/
|
||||
|
||||
/* istanbul ignore next */
|
||||
private onError(error: ErrnoException) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof this.port === 'string'
|
||||
? `Pipe ${this.port}`
|
||||
: `Port ${this.port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
Logger.error(`${bind} requires elevated privileges`);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
Logger.error(`${bind} is already in use`);
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
private onListening() {
|
||||
const addr = this.server.address();
|
||||
/* istanbul ignore next */
|
||||
const bind = typeof addr === 'string'
|
||||
? `pipe ${addr}` : addr === null
|
||||
? 'null' : `port ${addr.port}`;
|
||||
Logger.ok(`Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the route gets invoked
|
||||
*
|
||||
* Override this method for your own plugin
|
||||
*
|
||||
* @param req An express Request from the backend
|
||||
* @param res An express Response to the backend for you to send back data
|
||||
*/
|
||||
protected abstract async onRouteInvoke(req: express.Request, res: express.Response): Promise<void>;
|
||||
|
||||
/**
|
||||
* Closes the server
|
||||
*
|
||||
* This will stop the plugin from listening to any requests at all, and is currently an irreversible process.
|
||||
* This means, that the instantiated plugin is basically useless afterwards.
|
||||
*/
|
||||
public async close() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.close((err) => {
|
||||
/* istanbul ignore next */
|
||||
if (typeof err !== 'undefined') {
|
||||
/* istanbul ignore next */
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the plugin
|
||||
*
|
||||
* **THIS METHOD GETS CALLED AUTOMATICALLY WITH [[PluginClient.registerPlugin]]**
|
||||
* If the plugin is not started, it will return 404 on any route
|
||||
*/
|
||||
public start() {
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the plugin
|
||||
*
|
||||
* **THIS METHOD GETS CALLED AUTOMATICALLY WITH [[PluginClient.unregisterPlugin]]**
|
||||
* If the plugin is not started, it will return 404 on any route
|
||||
*/
|
||||
public stop() {
|
||||
// you can't unregister routes from express. So this is a workaround.
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,15 @@
|
||||
*/
|
||||
import {SCBulkAddRoute, SCBulkDoneRoute, SCDish, SCMessage, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||
import {expect} from 'chai';
|
||||
import * as chai from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as chaiSpies from 'chai-spies';
|
||||
import {suite, test} from 'mocha-typescript';
|
||||
import * as moment from 'moment';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import {suite, test} from '@testdeck/mocha';
|
||||
import moment from 'moment';
|
||||
import {Bulk} from '../src/bulk';
|
||||
import {Client} from '../src/client';
|
||||
import {BulkWithMultipleTypesError} from '../src/errors';
|
||||
import {HttpClient} from '../src/httpClient';
|
||||
import {HttpClient} from '../src/http-client';
|
||||
|
||||
chai.should();
|
||||
chai.use(chaiSpies);
|
||||
@@ -89,7 +89,10 @@ export class BulkSpec {
|
||||
audiences: [
|
||||
'students',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foobar',
|
||||
origin: {
|
||||
indexed: moment().format(),
|
||||
|
||||
@@ -28,14 +28,14 @@ import {
|
||||
SCThingType,
|
||||
} from '@openstapps/core';
|
||||
import {expect} from 'chai';
|
||||
import * as chai from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as chaiSpies from 'chai-spies';
|
||||
import {suite, test} from 'mocha-typescript';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import {suite, test} from '@testdeck/mocha';
|
||||
import {Client} from '../src/client';
|
||||
import {ApiError, OutOfRangeError} from '../src/errors';
|
||||
import {HttpClient} from '../src/httpClient';
|
||||
import {HttpClientResponse} from '../src/httpClientInterface';
|
||||
import {HttpClient} from '../src/http-client';
|
||||
import {HttpClientResponse} from '../src/http-client-interface';
|
||||
|
||||
chai.should();
|
||||
chai.use(chaiSpies);
|
||||
@@ -98,7 +98,7 @@ export class ClientSpec {
|
||||
}
|
||||
|
||||
@test
|
||||
async constructWithVersion() {
|
||||
async constructWithHeaders() {
|
||||
sandbox.on(httpClient, 'request', invokeIndexRoute);
|
||||
|
||||
expect(httpClient.request).not.to.have.been.first.called();
|
||||
@@ -109,6 +109,7 @@ export class ClientSpec {
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: {},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-StApps-Version': 'foo.foo.foo',
|
||||
},
|
||||
method: indexRoute.method,
|
||||
@@ -133,7 +134,10 @@ export class ClientSpec {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
metaData: {
|
||||
debug: true,
|
||||
platform: 'android',
|
||||
@@ -156,7 +160,9 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: feedback,
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: feedbackRoute.method,
|
||||
url: new URL('http://localhost' + feedbackRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -168,7 +174,10 @@ export class ClientSpec {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'foo',
|
||||
@@ -214,7 +223,9 @@ export class ClientSpec {
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: searchRoute.method,
|
||||
url: new URL('http://localhost' + searchRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -254,7 +265,10 @@ export class ClientSpec {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'foo',
|
||||
@@ -302,7 +316,9 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: {},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: indexRoute.method,
|
||||
url: new URL('http://localhost' + indexRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -330,7 +346,9 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: undefined,
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: indexRoute.method,
|
||||
url: new URL('http://localhost' + indexRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -389,7 +407,9 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: {a: {size: 1}, b: {size: 1}},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: multiSearchRoute.method,
|
||||
url: new URL('http://localhost' + multiSearchRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -437,13 +457,17 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: {foo: {size: 0}, bar: {size: 0}},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: multiSearchRoute.method,
|
||||
url: new URL('http://localhost' + multiSearchRoute.getUrlFragment()),
|
||||
});
|
||||
expect(httpClient.request).to.have.been.second.called.with({
|
||||
body: {foo: {size: 1000}, bar: {size: 500}, foobar: {size: 30}},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: multiSearchRoute.method,
|
||||
url: new URL('http://localhost' + multiSearchRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -504,7 +528,9 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: {size: 1},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: searchRoute.method,
|
||||
url: new URL('http://localhost' + searchRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -540,7 +566,9 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: {from: 30, size: 30},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: searchRoute.method,
|
||||
url: new URL('http://localhost' + searchRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -574,13 +602,17 @@ export class ClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: {size: 0},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: searchRoute.method,
|
||||
url: new URL('http://localhost' + searchRoute.getUrlFragment()),
|
||||
});
|
||||
expect(httpClient.request).to.have.been.second.called.with({
|
||||
body: {size: 1000},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: searchRoute.method,
|
||||
url: new URL('http://localhost' + searchRoute.getUrlFragment()),
|
||||
});
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool/lib/async-pool';
|
||||
import {
|
||||
isThing,
|
||||
SCBulkAddResponse,
|
||||
SCBulkAddRoute,
|
||||
SCBulkDoneResponse,
|
||||
@@ -20,21 +22,27 @@ import {
|
||||
SCBulkResponse,
|
||||
SCBulkRoute,
|
||||
SCMessage,
|
||||
SCThingOriginType,
|
||||
SCThingType,
|
||||
SCThingUpdateResponse,
|
||||
SCThingUpdateRoute,
|
||||
SCThingType,
|
||||
SCThingOriginType,
|
||||
SCThingWithoutReferences,
|
||||
} from '@openstapps/core';
|
||||
import * as chai from 'chai';
|
||||
import chai from 'chai';
|
||||
import {expect} from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as chaiSpies from 'chai-spies';
|
||||
import {suite, test} from 'mocha-typescript';
|
||||
import * as moment from 'moment';
|
||||
import {ConnectorClient} from '../src/connectorClient';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import clone = require('fast-clone');
|
||||
import {readdir, readFile} from 'fs';
|
||||
import {suite, test} from '@testdeck/mocha';
|
||||
import moment from 'moment';
|
||||
import {join, resolve} from 'path';
|
||||
import traverse from 'traverse';
|
||||
import {promisify} from 'util';
|
||||
import {ConnectorClient} from '../src/connector-client';
|
||||
import {EmptyBulkError, NamespaceNotDefinedError} from '../src/errors';
|
||||
import {HttpClient} from '../src/httpClient';
|
||||
import {HttpClientRequest, HttpClientResponse} from '../src/httpClientInterface';
|
||||
import {HttpClient} from '../src/http-client';
|
||||
import {HttpClientRequest, HttpClientResponse} from '../src/http-client-interface';
|
||||
|
||||
chai.should();
|
||||
chai.use(chaiSpies);
|
||||
@@ -47,8 +55,27 @@ const bulkDoneRoute = new SCBulkDoneRoute();
|
||||
const bulkRoute = new SCBulkRoute();
|
||||
const thingUpdateRoute = new SCThingUpdateRoute();
|
||||
|
||||
const readdirPromisified = promisify(readdir);
|
||||
const readFilePromisified = promisify(readFile);
|
||||
|
||||
const httpClient = new HttpClient();
|
||||
|
||||
/**
|
||||
* Check if something contains things
|
||||
*
|
||||
* @param thing Thing to check
|
||||
*/
|
||||
function doesContainThings<T extends SCThingWithoutReferences>(thing: T): boolean {
|
||||
/* tslint:disable-next-line:only-arrow-functions */
|
||||
return traverse(thing).reduce(function (sum, item) {
|
||||
if (this.isRoot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return sum || (item === null) ? false : isThing(item);
|
||||
}, false);
|
||||
}
|
||||
|
||||
@suite()
|
||||
export class ConnectorClientSpec {
|
||||
async after() {
|
||||
@@ -82,7 +109,9 @@ export class ConnectorClientSpec {
|
||||
source: 'foo',
|
||||
type: SCThingType.Message,
|
||||
},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: bulkRoute.method,
|
||||
url: new URL('http://localhost' + bulkRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -115,7 +144,9 @@ export class ConnectorClientSpec {
|
||||
source: 'foo',
|
||||
type: SCThingType.Message,
|
||||
},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: bulkRoute.method,
|
||||
url: new URL('http://localhost' + bulkRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -128,7 +159,10 @@ export class ConnectorClientSpec {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'foo',
|
||||
@@ -142,7 +176,10 @@ export class ConnectorClientSpec {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'foo',
|
||||
@@ -196,7 +233,9 @@ export class ConnectorClientSpec {
|
||||
source: 'copy',
|
||||
type: SCThingType.Message,
|
||||
},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: bulkRoute.method,
|
||||
url: new URL('http://localhost' + bulkRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -215,7 +254,10 @@ export class ConnectorClientSpec {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'foo',
|
||||
@@ -229,7 +271,10 @@ export class ConnectorClientSpec {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'foo',
|
||||
@@ -283,7 +328,9 @@ export class ConnectorClientSpec {
|
||||
source: 'stapps-api',
|
||||
type: SCThingType.Message,
|
||||
},
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: bulkRoute.method,
|
||||
url: new URL('http://localhost' + bulkRoute.getUrlFragment()),
|
||||
});
|
||||
@@ -305,13 +352,82 @@ export class ConnectorClientSpec {
|
||||
}).to.throw(NamespaceNotDefinedError);
|
||||
}
|
||||
|
||||
@test
|
||||
async removeReferences() {
|
||||
const pathToTestFiles = resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'node_modules',
|
||||
'@openstapps',
|
||||
'core',
|
||||
'test',
|
||||
'resources',
|
||||
'indexable'
|
||||
);
|
||||
|
||||
const testFiles = await readdirPromisified(pathToTestFiles);
|
||||
|
||||
const testInstances = await asyncPool(5, testFiles, async (testFile) => {
|
||||
const buffer = await readFilePromisified(join(pathToTestFiles, testFile));
|
||||
const content = JSON.parse(buffer.toString());
|
||||
|
||||
return content.instance;
|
||||
});
|
||||
|
||||
for (const testInstance of testInstances) {
|
||||
|
||||
const checkInstance = clone(testInstance);
|
||||
const testInstanceWithoutReferences = ConnectorClient.removeReferences(testInstance);
|
||||
|
||||
expect(doesContainThings(testInstanceWithoutReferences)).to.be
|
||||
.equal(false, JSON.stringify(
|
||||
[testInstance, testInstanceWithoutReferences],
|
||||
null,
|
||||
2,
|
||||
));
|
||||
expect((testInstanceWithoutReferences as any).origin).to.be
|
||||
.equal(undefined, JSON.stringify(
|
||||
[testInstance, testInstanceWithoutReferences],
|
||||
null,
|
||||
2,
|
||||
));
|
||||
expect(testInstance).to.be.deep
|
||||
.equal(checkInstance,
|
||||
'Removing the references of a thing could have side effects because no deep copy is used');
|
||||
}
|
||||
}
|
||||
|
||||
@test
|
||||
async removeUndefinedProperties() {
|
||||
const objectWithUndefinedProperties = {value: 'foo',
|
||||
novalue: undefined,
|
||||
nested: {
|
||||
value: 'foo',
|
||||
novalue: undefined},
|
||||
};
|
||||
const objectWithoutUndefinedProperties = {value: 'foo',
|
||||
nested: {
|
||||
value: 'foo'},
|
||||
};
|
||||
ConnectorClient.removeUndefinedProperties(objectWithUndefinedProperties);
|
||||
|
||||
expect(objectWithUndefinedProperties).to.deep.equal(objectWithoutUndefinedProperties, JSON.stringify(
|
||||
[objectWithUndefinedProperties, objectWithoutUndefinedProperties],
|
||||
null,
|
||||
2,
|
||||
));
|
||||
}
|
||||
|
||||
@test
|
||||
async update() {
|
||||
const message: SCMessage = {
|
||||
audiences: [
|
||||
'employees',
|
||||
],
|
||||
message: 'Lorem ipsum.',
|
||||
categories: [
|
||||
'news'
|
||||
],
|
||||
messageBody: 'Lorem ipsum.',
|
||||
name: 'foo',
|
||||
origin: {
|
||||
indexed: 'foo',
|
||||
@@ -337,7 +453,9 @@ export class ConnectorClientSpec {
|
||||
|
||||
expect(httpClient.request).to.have.been.called.with({
|
||||
body: message,
|
||||
headers: {},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: thingUpdateRoute.method,
|
||||
url: new URL('http://localhost' + thingUpdateRoute.getUrlFragment({
|
||||
TYPE: SCThingType.Message,
|
||||
@@ -24,14 +24,14 @@ import {
|
||||
SCSearchRoute,
|
||||
SCThingType,
|
||||
} from '@openstapps/core';
|
||||
import * as chai from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as chaiSpies from 'chai-spies';
|
||||
import {suite, test} from 'mocha-typescript';
|
||||
import * as moment from 'moment';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import {suite, test} from '@testdeck/mocha';
|
||||
import moment from 'moment';
|
||||
import {copy} from '../src/copy';
|
||||
import {ApiError} from '../src/errors';
|
||||
import {HttpClient, RequestOptions, Response} from '../src/httpClient';
|
||||
import {HttpClient, RequestOptions, Response} from '../src/http-client';
|
||||
import {RecursivePartial} from './client.spec';
|
||||
|
||||
chai.should();
|
||||
|
||||
207
test/e2e.spec.ts
Normal file
207
test/e2e.spec.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
// tslint:disable: completed-docs no-implicit-dependencies prefer-function-over-method newline-per-chained-call member-ordering
|
||||
import {
|
||||
SCBulkAddResponse,
|
||||
SCBulkAddRoute,
|
||||
SCBulkDoneResponse,
|
||||
SCBulkDoneRoute,
|
||||
SCBulkResponse,
|
||||
SCBulkRoute,
|
||||
SCSearchResponse,
|
||||
SCSearchRoute,
|
||||
SCThing,
|
||||
SCThings,
|
||||
} from '@openstapps/core';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import clone = require('fast-clone');
|
||||
import {existsSync, mkdirSync, rmdirSync, unlinkSync} from 'fs';
|
||||
import {createFileSync} from 'fs-extra';
|
||||
import {suite, test} from '@testdeck/mocha';
|
||||
import {join} from 'path';
|
||||
import {e2eRun, getItemsFromSamples} from '../src/e2e';
|
||||
import {ApiError} from '../src/errors';
|
||||
import {HttpClient, RequestOptions, Response} from '../src/http-client';
|
||||
import {RecursivePartial} from './client.spec';
|
||||
|
||||
chai.should();
|
||||
chai.use(chaiSpies);
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
const sandbox = chai.spy.sandbox();
|
||||
|
||||
const bulkRoute = new SCBulkRoute();
|
||||
const bulkAddRoute = new SCBulkAddRoute();
|
||||
const bulkDoneRoute = new SCBulkDoneRoute();
|
||||
|
||||
const searchRoute = new SCSearchRoute();
|
||||
|
||||
const httpClient = new HttpClient();
|
||||
|
||||
const storedThings: Map<string, SCThings> = new Map();
|
||||
|
||||
@suite
|
||||
export class E2EConnectorSpec {
|
||||
async after() {
|
||||
sandbox.restore();
|
||||
}
|
||||
|
||||
@test
|
||||
async getCoreTestSamples() {
|
||||
const items = await getItemsFromSamples('./node_modules/@openstapps/core/test/resources');
|
||||
// tslint:disable-next-line: no-unused-expression
|
||||
chai.expect(items).to.not.be.a.instanceof(Error);
|
||||
// tslint:disable-next-line: no-unused-expression
|
||||
chai.expect(items).to.not.be.empty;
|
||||
}
|
||||
|
||||
@test
|
||||
async getCoreTestSamplesShouldFail() {
|
||||
return getItemsFromSamples('./nonexistantdirectory')
|
||||
.then(<T extends SCThing>(items: T[]) => {
|
||||
// tslint:disable-next-line: no-unused-expression
|
||||
chai.expect(items).to.be.a.instanceof(Error);
|
||||
});
|
||||
}
|
||||
|
||||
@test
|
||||
async e2eRunSimulation() {
|
||||
type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse | SCSearchResponse>;
|
||||
|
||||
let failOnCompare = false;
|
||||
let failOnLookup = false;
|
||||
|
||||
sandbox.on(httpClient, 'request', async (request: RequestOptions): Promise<RecursivePartial<responses>> => {
|
||||
if (request.url.toString() === `http://localhost${bulkRoute.getUrlFragment().toString()}`) {
|
||||
|
||||
return {
|
||||
body: {
|
||||
state: 'in progress',
|
||||
uid: 'foo',
|
||||
},
|
||||
statusCode: bulkRoute.statusCodeSuccess,
|
||||
};
|
||||
}
|
||||
|
||||
if (request.url.toString() === `http://localhost${bulkAddRoute.getUrlFragment({UID: 'foo'}).toString()}`) {
|
||||
storedThings.set(request.body.uid, clone(request.body));
|
||||
|
||||
return {
|
||||
body: {},
|
||||
statusCode: bulkAddRoute.statusCodeSuccess,
|
||||
};
|
||||
}
|
||||
|
||||
if (request.url.toString() === `http://localhost${bulkDoneRoute.getUrlFragment({UID: 'foo'}).toString()}`) {
|
||||
return {
|
||||
body: {},
|
||||
statusCode: bulkDoneRoute.statusCodeSuccess,
|
||||
};
|
||||
}
|
||||
|
||||
if (request.url.toString() === `http://localhost${searchRoute.getUrlFragment().toString()}`) {
|
||||
const thing = storedThings.get(request.body.filter.arguments.value);
|
||||
if (failOnCompare) {
|
||||
thing!.origin!.modified = 'altered';
|
||||
}
|
||||
const returnThing = failOnLookup ? [] : [thing];
|
||||
const returnBody = {
|
||||
data: returnThing,
|
||||
facets: [],
|
||||
pagination: {
|
||||
count: returnThing.length,
|
||||
offset: 0,
|
||||
total: returnThing.length,
|
||||
},
|
||||
stats: {
|
||||
time: 42,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
body: returnBody,
|
||||
statusCode: searchRoute.statusCodeSuccess,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
body: {},
|
||||
statusCode: searchRoute.statusCodeSuccess,
|
||||
};
|
||||
});
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
await e2eRun(httpClient, {to: 'http://localhost', samplesLocation: './node_modules/@openstapps/core/test/resources'});
|
||||
|
||||
failOnLookup = true;
|
||||
failOnCompare = false;
|
||||
// tslint:disable-next-line: max-line-length
|
||||
await e2eRun(httpClient, {to: 'http://localhost', samplesLocation: './node_modules/@openstapps/core/test/resources'})
|
||||
.should.be.rejectedWith('Search for single SCThing with uid');
|
||||
|
||||
failOnLookup = false;
|
||||
failOnCompare = true;
|
||||
// tslint:disable-next-line: max-line-length
|
||||
await e2eRun(httpClient, {to: 'http://localhost', samplesLocation: './node_modules/@openstapps/core/test/resources'})
|
||||
.should.be.rejectedWith('Unexpected difference');
|
||||
|
||||
}
|
||||
|
||||
@test
|
||||
async indexShouldFail() {
|
||||
type responses = Response<SCBulkAddResponse | SCBulkDoneResponse | SCBulkResponse>;
|
||||
|
||||
sandbox.on(httpClient, 'request', async (): Promise<RecursivePartial<responses>> => {
|
||||
|
||||
return {
|
||||
body: {},
|
||||
statusCode: Number.MAX_SAFE_INTEGER,
|
||||
};
|
||||
});
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
return e2eRun(httpClient, {to: 'http://localhost', samplesLocation: './node_modules/@openstapps/core/test/resources'})
|
||||
.should.be.rejectedWith(ApiError);
|
||||
}
|
||||
|
||||
@test
|
||||
async indexShouldFailDirectoryWithoutData() {
|
||||
const emptyDirPath = join(__dirname, 'emptyDir');
|
||||
if (!existsSync(emptyDirPath)) {
|
||||
mkdirSync(emptyDirPath);
|
||||
}
|
||||
await e2eRun(httpClient, {to: 'http://localhost', samplesLocation: emptyDirPath})
|
||||
.should.be.rejectedWith('Could not index samples. None were retrieved from the file system.');
|
||||
rmdirSync(emptyDirPath);
|
||||
}
|
||||
|
||||
@test
|
||||
async indexShouldFailDirectoryWithoutJsonData() {
|
||||
const somewhatFilledDirPath = join(__dirname, 'somewhatFilledDir');
|
||||
if (!existsSync(somewhatFilledDirPath)) {
|
||||
mkdirSync(somewhatFilledDirPath);
|
||||
}
|
||||
const nonJsonFile = join (somewhatFilledDirPath, 'nonjson.txt');
|
||||
createFileSync(nonJsonFile);
|
||||
await e2eRun(httpClient, {to: 'http://localhost', samplesLocation: somewhatFilledDirPath})
|
||||
.should.be.rejectedWith('Could not index samples. None were retrieved from the file system.');
|
||||
unlinkSync(nonJsonFile);
|
||||
rmdirSync(somewhatFilledDirPath);
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,11 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import * as chai from 'chai';
|
||||
import chai from 'chai';
|
||||
import {expect} from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as chaiSpies from 'chai-spies';
|
||||
import {suite, test} from 'mocha-typescript';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import {suite, test} from '@testdeck/mocha';
|
||||
import {ApiError} from '../src/errors';
|
||||
|
||||
chai.should();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2018-2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -12,25 +12,15 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import * as chai from 'chai';
|
||||
import {expect} from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as chaiSpies from 'chai-spies';
|
||||
import {suite, test} from 'mocha-typescript';
|
||||
import * as nock from 'nock';
|
||||
import {HttpClient} from '../src/httpClient';
|
||||
import {suite, test} from '@testdeck/mocha';
|
||||
import nock from 'nock';
|
||||
import {HttpClient} from '../src/http-client';
|
||||
|
||||
chai.should();
|
||||
chai.use(chaiSpies);
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
const sandbox = chai.spy.sandbox();
|
||||
// TODO: use after each to clean up the nock (then there is no need for numerated resource links)
|
||||
|
||||
@suite()
|
||||
export class ConnectorClientSpec {
|
||||
async after() {
|
||||
sandbox.restore();
|
||||
}
|
||||
export class HttpClientSpec {
|
||||
|
||||
@test
|
||||
async construct() {
|
||||
@@ -39,6 +29,10 @@ export class ConnectorClientSpec {
|
||||
}).not.to.throw();
|
||||
}
|
||||
|
||||
async after() {
|
||||
nock.cleanAll();
|
||||
}
|
||||
|
||||
@test
|
||||
async request() {
|
||||
const client = new HttpClient();
|
||||
@@ -63,10 +57,7 @@ export class ConnectorClientSpec {
|
||||
.reply(200, 'foo');
|
||||
|
||||
const response = await client.request({
|
||||
body: {
|
||||
foo: 'bar',
|
||||
},
|
||||
url: new URL('http://www.example.com/resource'),
|
||||
url: new URL('http://www.example.com/resource')
|
||||
});
|
||||
|
||||
expect(response.body).to.be.equal('foo');
|
||||
@@ -75,17 +66,24 @@ export class ConnectorClientSpec {
|
||||
@test
|
||||
async requestWithError() {
|
||||
const client = new HttpClient();
|
||||
let caughtErr;
|
||||
|
||||
nock('http://www.example.com')
|
||||
.get('/resource')
|
||||
.replyWithError('foo');
|
||||
|
||||
return client.request({
|
||||
body: {
|
||||
foo: 'bar',
|
||||
},
|
||||
url: new URL('http://www.example.com/resource'),
|
||||
}).should.be.rejected;
|
||||
try {
|
||||
await client.request({
|
||||
body: {
|
||||
foo: 'bar',
|
||||
},
|
||||
url: new URL('http://www.example.com/resource'),
|
||||
});
|
||||
} catch (err) {
|
||||
caughtErr = err;
|
||||
}
|
||||
|
||||
expect(caughtErr).not.to.be.undefined;
|
||||
}
|
||||
|
||||
@test
|
||||
118
test/plugin-client.spec.ts
Normal file
118
test/plugin-client.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {SCPluginRegisterRequest, SCPluginRegisterResponse, SCPluginRegisterRoute} from '@openstapps/core';
|
||||
import chai from 'chai';
|
||||
import {expect} from 'chai';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import {suite, test, timeout} from '@testdeck/mocha';
|
||||
import {HttpClient} from '../src/http-client';
|
||||
import {HttpClientResponse} from '../src/http-client-interface';
|
||||
import {PluginClient} from '../src/plugin-client';
|
||||
import {TestPlugin} from './plugin-resources/test-plugin';
|
||||
|
||||
chai.use(chaiSpies);
|
||||
|
||||
const sandbox = chai.spy.sandbox();
|
||||
|
||||
const httpClient = new HttpClient();
|
||||
|
||||
const pluginRegisterRoute = new SCPluginRegisterRoute();
|
||||
|
||||
const pluginClient = new PluginClient(httpClient, 'http://localhost');
|
||||
|
||||
@suite(timeout(10000))
|
||||
export class PluginClientSpec {
|
||||
static plugin: TestPlugin;
|
||||
|
||||
static async after() {
|
||||
await this.plugin.close();
|
||||
}
|
||||
|
||||
static async before() {
|
||||
this.plugin = new TestPlugin(4000, '', '', '', '', {getSchema: () => {/***/}} as any, '', '', '');
|
||||
}
|
||||
|
||||
async after() {
|
||||
sandbox.restore();
|
||||
}
|
||||
|
||||
@test
|
||||
async registerPlugin() {
|
||||
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCPluginRegisterResponse>> => {
|
||||
return {
|
||||
body: {
|
||||
success: true,
|
||||
},
|
||||
headers: {},
|
||||
statusCode: pluginRegisterRoute.statusCodeSuccess,
|
||||
};
|
||||
});
|
||||
|
||||
expect(httpClient.request).not.to.have.been.called();
|
||||
|
||||
await pluginClient.registerPlugin(PluginClientSpec.plugin);
|
||||
|
||||
const request: SCPluginRegisterRequest = {
|
||||
action: 'add',
|
||||
plugin: {
|
||||
address: PluginClientSpec.plugin.fullUrl,
|
||||
name: PluginClientSpec.plugin.name,
|
||||
requestSchema: PluginClientSpec.plugin.requestSchema,
|
||||
responseSchema: PluginClientSpec.plugin.responseSchema,
|
||||
route: PluginClientSpec.plugin.route,
|
||||
},
|
||||
};
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: request,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: pluginRegisterRoute.method,
|
||||
url: new URL(`http://localhost${pluginRegisterRoute.getUrlFragment()}`),
|
||||
});
|
||||
}
|
||||
|
||||
@test
|
||||
async unregisterPlugin() {
|
||||
sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCPluginRegisterResponse>> => {
|
||||
return {
|
||||
body: {
|
||||
success: true,
|
||||
},
|
||||
headers: {},
|
||||
statusCode: pluginRegisterRoute.statusCodeSuccess,
|
||||
};
|
||||
});
|
||||
|
||||
expect(httpClient.request).not.to.have.been.called();
|
||||
|
||||
await pluginClient.unregisterPlugin(PluginClientSpec.plugin);
|
||||
|
||||
const request: SCPluginRegisterRequest = {
|
||||
action: 'remove',
|
||||
route: PluginClientSpec.plugin.route,
|
||||
};
|
||||
|
||||
expect(httpClient.request).to.have.been.first.called.with({
|
||||
body: request,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: pluginRegisterRoute.method,
|
||||
url: new URL(`http://localhost${pluginRegisterRoute.getUrlFragment()}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
25
test/plugin-resources/test-plugin-response.ts
Normal file
25
test/plugin-resources/test-plugin-response.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* The Response Interface
|
||||
*
|
||||
* @validatable
|
||||
*/
|
||||
export interface TestPluginResponse {
|
||||
/**
|
||||
* Query dummy
|
||||
*/
|
||||
query: string;
|
||||
}
|
||||
32
test/plugin-resources/test-plugin.ts
Normal file
32
test/plugin-resources/test-plugin.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as express from 'express';
|
||||
import {Plugin} from '../../src/plugin';
|
||||
|
||||
/**
|
||||
* A test plugin we use for all the tests
|
||||
*
|
||||
* It can be constructed without any parameter at all, or with all parameters if we want to test it
|
||||
* It also serves as kind of a minimal plugin
|
||||
*/
|
||||
export class TestPlugin extends Plugin {
|
||||
// tslint:disable-next-line: completed-docs prefer-function-over-method
|
||||
protected async onRouteInvoke(_req: express.Request, res: express.Response): Promise<void> {
|
||||
res.json({});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
141
test/plugin.spec.ts
Normal file
141
test/plugin.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Converter} from '@openstapps/core-tools/lib/schema';
|
||||
import chai from 'chai';
|
||||
import {expect} from 'chai';
|
||||
import chaiSpies from 'chai-spies';
|
||||
import {readFileSync} from 'fs';
|
||||
import {suite, test, timeout} from '@testdeck/mocha';
|
||||
import {resolve} from 'path';
|
||||
import {HttpClient} from '../src/http-client';
|
||||
import {TestPlugin} from './plugin-resources/test-plugin';
|
||||
|
||||
chai.use(chaiSpies);
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const sandbox = chai.spy.sandbox();
|
||||
|
||||
const httpClient = new HttpClient();
|
||||
|
||||
@suite(timeout(20000))
|
||||
export class PluginSpec {
|
||||
static testPlugin: TestPlugin;
|
||||
|
||||
static async after() {
|
||||
PluginSpec.testPlugin.close();
|
||||
}
|
||||
|
||||
static async before() {
|
||||
PluginSpec.testPlugin = new TestPlugin(4000, '', '', '', '', {
|
||||
getSchema: () => {/***/
|
||||
},
|
||||
} as any, '', '', '');
|
||||
}
|
||||
|
||||
async after() {
|
||||
sandbox.restore();
|
||||
}
|
||||
|
||||
@test
|
||||
async construct() {
|
||||
const converter = new Converter(__dirname, resolve(__dirname,'plugin-resources','test-plugin-response.ts'));
|
||||
|
||||
sandbox.on(converter, 'getSchema', (schemaName) => {
|
||||
return {$id: schemaName};
|
||||
});
|
||||
|
||||
const constructTestPlugin = new TestPlugin(
|
||||
4001,
|
||||
'A',
|
||||
'http://B',
|
||||
'/C', // this doesn't matter for our tests, it's only something that affects the backend
|
||||
'http://D',
|
||||
// @ts-ignore fake converter is not a converter
|
||||
converter,
|
||||
'PluginTestRequest',
|
||||
'PluginTestResponse',
|
||||
JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json')).toString()).version,
|
||||
);
|
||||
expect(constructTestPlugin.port).to.be.equal(4001);
|
||||
expect(constructTestPlugin.name).to.be.equal('A');
|
||||
expect(constructTestPlugin.url).to.be.equal('http://B');
|
||||
expect(constructTestPlugin.route).to.be.equal('/C');
|
||||
// @ts-ignore backendUrl is protected
|
||||
expect(constructTestPlugin.backendUrl).to.be.equal('http://D');
|
||||
// schemas are already covered, together with the directory and version
|
||||
// @ts-ignore active is private
|
||||
expect(constructTestPlugin.active).to.be.equal(false);
|
||||
expect(constructTestPlugin.requestSchema.$id).to.be.equal('PluginTestRequest');
|
||||
expect(constructTestPlugin.responseSchema.$id).to.be.equal('PluginTestResponse');
|
||||
|
||||
sandbox.on(constructTestPlugin, 'onRouteInvoke');
|
||||
await httpClient.request({
|
||||
url: new URL('http://localhost:4001'),
|
||||
});
|
||||
// onRouteInvoke is a protected method, but we need to access it from the outside to test it
|
||||
// @ts-ignore
|
||||
expect(constructTestPlugin.onRouteInvoke).not.to.have.been.called();
|
||||
|
||||
await constructTestPlugin.close();
|
||||
sandbox.restore(constructTestPlugin, 'onRouteInvoke');
|
||||
}
|
||||
|
||||
@test
|
||||
async fullUrl() {
|
||||
const constructTestPlugin = new TestPlugin(4001, '', 'http://B', '', '', {
|
||||
getSchema: () => {/***/
|
||||
},
|
||||
} as any, '', '', '');
|
||||
expect(constructTestPlugin.fullUrl).to.be.equal('http://B:4001');
|
||||
await constructTestPlugin.close();
|
||||
}
|
||||
|
||||
@test
|
||||
async start() {
|
||||
PluginSpec.testPlugin.start();
|
||||
|
||||
sandbox.on(PluginSpec.testPlugin, 'onRouteInvoke');
|
||||
|
||||
await httpClient.request({
|
||||
url: new URL('http://localhost:4000'),
|
||||
});
|
||||
|
||||
// onRouteInvoke is a protected method, but we need to access it from the outside to test it
|
||||
// @ts-ignore
|
||||
expect(PluginSpec.testPlugin.onRouteInvoke).to.have.been.called();
|
||||
}
|
||||
|
||||
@test
|
||||
async stop() {
|
||||
// simulate a normal use case by first starting the plugin and then stopping it
|
||||
PluginSpec.testPlugin.start();
|
||||
PluginSpec.testPlugin.stop();
|
||||
|
||||
sandbox.on(PluginSpec.testPlugin, 'onRouteInvoke');
|
||||
|
||||
const response = await httpClient.request({
|
||||
url: new URL('http://localhost:4000'),
|
||||
});
|
||||
|
||||
await expect(response.statusCode).to.be.equal(404);
|
||||
// onRouteInvoke is a protected method, but we need to access it from the outside to test it
|
||||
// @ts-ignore
|
||||
expect(PluginSpec.testPlugin.onRouteInvoke).not.to.have.been.called();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user