Compare commits

..

138 Commits

Author SHA1 Message Date
Rainer Killinger
da056b75d0 0.31.1 2021-08-27 14:42:50 +02:00
Rainer Killinger
51c157d577 fix: plugin related tests 2021-08-27 14:37:05 +02:00
Rainer Killinger
9f744a3c88 test: adjust path to core resource files 2021-08-26 15:37:40 +02:00
openstappsbot
d6823a1206 refactor: update all 2021-08-26 07:07:06 +00:00
Jovan Krunić
beb57b2f5a docs: update changelog 2021-08-17 15:55:23 +02:00
Jovan Krunić
0bda134bfe 0.31.0 2021-08-17 15:55:22 +02:00
Jovan Krunić
dbcde48c14 refactor: adjust tests to core changes 2021-08-17 15:18:09 +02:00
openstappsbot
388e4fbb76 refactor: update all 2021-08-13 13:06:04 +00:00
Rainer Killinger
9463130e7d docs: update changelog 2021-08-04 11:08:41 +02:00
Rainer Killinger
3a3cf835a5 0.30.0 2021-08-04 11:08:40 +02:00
Rainer Killinger
0a10ef1362 test: adjust istanbul ignore statements 2021-08-04 11:05:08 +02:00
openstappsbot
546aa14895 refactor: update all 2021-08-04 10:51:30 +02:00
Wieland Schoebl
4955d8b6be docs: update changelog 2021-06-08 15:22:45 +02:00
Wieland Schoebl
08c2a6c8ab 0.29.0 2021-06-08 15:22:39 +02:00
openstappsbot
dbea099d92 refactor: update all 2021-06-01 07:09:18 +00:00
Rainer Killinger
4ead266a07 docs: update changelog 2021-04-27 13:38:01 +02:00
Rainer Killinger
6f377264e9 0.28.0 2021-04-27 13:38:00 +02:00
Rainer Killinger
a599c87384 refactor: update dependencies 2021-04-26 13:19:14 +02:00
openstappsbot
9566f7aa0a refactor: update all 2021-04-07 12:27:50 +00:00
Rainer Killinger
af965bce7a refactor: update @types/node for node version 14 2021-04-06 14:53:34 +02:00
openstappsbot
d61be19a18 refactor: update all 2021-03-29 04:13:55 +00:00
Rainer Killinger
42384f0ed0 docs: update changelog 2021-03-08 12:25:54 +01:00
Rainer Killinger
0219d0571b 0.27.0 2021-03-08 12:25:52 +01:00
Rainer Killinger
8043752146 build: update package-lock.json 2021-03-08 12:25:29 +01:00
Jovan Krunić
29cd22f2d1 fix: send valid JSON on bulk/done
Closes #38
2021-03-03 17:54:05 +01:00
Rainer Killinger
5abdd9f98f docs: update changelog 2021-02-23 12:18:11 +01:00
Rainer Killinger
327e49ec88 0.26.0 2021-02-23 12:18:07 +01:00
openstappsbot
13857cd66e refactor: update all 2021-02-23 11:10:31 +00:00
Jovan Krunić
9a939e9ccd refactor: replace deprecated request library with got
Closes #35
2020-12-17 10:38:30 +01:00
Rainer Killinger
dfc0a3aed9 docs: update changelog 2020-11-09 10:57:05 +01:00
Rainer Killinger
2cd2513f64 0.25.0 2020-11-09 10:57:04 +01:00
Jovan Krunić
9067d7d6cf docs: update changelog 2020-11-09 10:45:21 +01:00
Jovan Krunić
f0c54777eb 0.24.0 2020-11-09 10:45:21 +01:00
Michel Jonathan Schmitz
c8ceefda80 ci: remove caching 2020-11-09 10:45:12 +01:00
Michel Jonathan Schmitz
2822aac652 build: update dependencies 2020-11-02 09:35:49 +01:00
Jovan Krunić
92a21052f1 docs: update changelog 2020-10-23 16:58:28 +02:00
Jovan Krunić
8013d73fe4 0.23.0 2020-10-23 16:58:27 +02:00
Wieland Schoebl
b3e107528b fix: make express parse body 2020-07-17 14:20:11 +02:00
Frank Nagel
9a93ea78c3 ci: Change 'npm audit' failure behaviour
The audit fails only if the results include a vulnerability with a level of
at least 'high' in scheduled pipelines.
2020-06-19 12:56:41 +02:00
Michel Jonathan Schmitz
1913e89366 docs: update changelog 2020-05-15 09:26:25 +02:00
Michel Jonathan Schmitz
8b8088545b 0.22.0 2020-05-15 09:26:25 +02:00
Michel Jonathan Schmitz
9f9167c9c7 test: update test files 2020-05-13 08:47:30 +02:00
Michel Jonathan Schmitz
d30de896f0 feat: add content-type header 2020-05-13 08:47:21 +02:00
Michel Jonathan Schmitz
e0ee3258e4 build: update peer dependency 2020-05-12 13:27:34 +02:00
Rainer Killinger
e5c9d06ef8 docs: update changelog 2020-05-06 10:57:32 +02:00
Rainer Killinger
fa2b118545 0.21.0 2020-05-06 10:57:30 +02:00
Rainer Killinger
99a231800c docs: update changelog 2020-05-06 10:55:32 +02:00
Rainer Killinger
393c50a685 0.20.0 2020-05-06 10:55:32 +02:00
Rainer Killinger
3fc0bb0a1f refactor: update dependencies 2020-05-06 10:55:32 +02:00
Rainer Killinger
b98f856b89 ci: cleanup and add image tag with core version 2020-05-06 10:55:24 +02:00
Rainer Killinger
c570d39637 docs: update changelog 2020-03-11 14:52:46 +01:00
Rainer Killinger
5815ad8ffd 0.19.0 2020-03-11 14:52:40 +01:00
Rainer Killinger
4ae968ff0f fix: repair and unify subcommand help outputs 2020-03-10 13:35:13 +01:00
Rainer Killinger
e18858fc58 feat: add option to wait on resource to e2e script 2020-03-10 13:35:13 +01:00
Rainer Killinger
3196cd986e refactor: update dependencies 2020-03-10 13:35:11 +01:00
Rainer Killinger
5456d4e7c0 ci: seperate audit jobs from test stage 2020-03-02 15:40:46 +01:00
Rainer Killinger
d364df38de docs: update changelog 2020-02-04 16:30:39 +01:00
Rainer Killinger
d45c4b7154 0.18.0 2020-02-04 16:30:28 +01:00
Rainer Killinger
466836cfd0 refactor: generalize docker image name 2019-12-10 13:56:19 +01:00
Rainer Killinger
dc79dc8feb feat: extend e2e procedure 2019-12-10 13:38:39 +01:00
Rainer Killinger
91de58b5ae refactor: update dependencies 2019-12-10 13:38:29 +01:00
Sebastian Lange
86709a3465 docs: update changelog 2019-11-15 15:26:26 +01:00
Sebastian Lange
13b4d34341 0.17.0 2019-11-15 15:26:25 +01:00
Sebastian Lange
a0731bc26d refactor: update dependencies 2019-11-14 16:28:35 +01:00
Wieland Schöbl
238ceaaaa9 docs: update changelog 2019-09-03 13:46:12 +02:00
Wieland Schöbl
52c2ee5d67 0.16.0 2019-09-03 13:46:11 +02:00
Wieland Schöbl
3baeff23ca build: update dependencies 2019-09-03 13:44:21 +02:00
Wieland Schöbl
c2848fc7a5 feat: add plugin class
Fixes #12
2019-08-21 14:53:23 +02:00
Karl-Philipp Wulfert
e32da822e1 docs: update changelog 2019-08-19 13:10:35 +02:00
Karl-Philipp Wulfert
cc6e29619d 0.15.0 2019-08-19 13:10:34 +02:00
Karl-Philipp Wulfert
963b0f897c build: update dependencies 2019-08-19 13:10:04 +02:00
Karl-Philipp Wulfert
4f47409b54 docs: update changelog 2019-07-23 11:51:15 +02:00
Karl-Philipp Wulfert
a255a50a19 0.14.0 2019-07-23 11:51:15 +02:00
Karl-Philipp Wulfert
28500f64be build: update dependencies 2019-07-23 11:50:53 +02:00
Karl-Philipp Wulfert
69b3395a1c docs: update changelog 2019-07-16 14:49:11 +02:00
Karl-Philipp Wulfert
312890f7d8 0.13.0 2019-07-16 14:49:11 +02:00
Rainer Killinger
6b92007e3e ci: adjust .gitlab-ci.yml to latest convention 2019-07-16 14:39:31 +02:00
Rainer Killinger
3a5062d9a9 test: add test for fixed side effects 2019-07-16 14:35:39 +02:00
Roman Klopsch
ded221c175 fix: change removeReferences() to also remove origin
In addition remove unwanted side effects from the function
2019-07-16 14:34:11 +02:00
Karl-Philipp Wulfert
24f28346bf docs: update changelog 2019-07-15 11:52:07 +02:00
Karl-Philipp Wulfert
698fd6fa8f 0.12.0 2019-07-15 11:52:06 +02:00
Karl-Philipp Wulfert
32eb493f13 build: update dependencies 2019-07-15 11:51:43 +02:00
Rainer Killinger
f95fd09112 feat: add function to remove undefined properties 2019-07-15 11:15:24 +02:00
Karl-Philipp Wulfert
1958f7f0b4 docs: update changelog 2019-07-12 15:47:43 +02:00
Karl-Philipp Wulfert
3220739ae4 0.11.0 2019-07-12 15:47:42 +02:00
Karl-Philipp Wulfert
6f3b122231 build: update dependencies 2019-07-12 15:47:18 +02:00
Karl-Philipp Wulfert
e5659c1aa6 docs: update changelog 2019-07-03 15:43:09 +02:00
Karl-Philipp Wulfert
d50addf563 0.10.0 2019-07-03 15:43:09 +02:00
Karl-Philipp Wulfert
fd0bab6130 build: update dependencies 2019-07-03 15:42:28 +02:00
Karl-Philipp Wulfert
337ed38d9c docs: update changelog 2019-06-20 11:24:10 +02:00
Karl-Philipp Wulfert
c1aa5b4661 0.9.0 2019-06-20 11:24:09 +02:00
Michel Jonathan Schmitz
6fb58f3790 test: add test for full coverage testing 2019-06-19 16:14:51 +02:00
Michel Jonathan Schmitz
45755000f3 style: apply stricter ts lint rules 2019-06-19 16:13:50 +02:00
Michel Jonathan Schmitz
38f5445634 build: update dependencies 2019-06-19 16:11:52 +02:00
Michel Jonathan Schmitz
bafcabf7cb test: create the empty test directory via code 2019-06-04 10:36:12 +02:00
Michel Jonathan Schmitz
aadd424400 test: improve coverage of tests for e2e 2019-05-21 13:36:01 +02:00
Michel Jonathan Schmitz
ec6296a606 refactor: update e2e methods 2019-05-21 13:35:51 +02:00
Karl-Philipp Wulfert
2c43fc09b5 docs: update changelog 2019-05-14 16:26:51 +02:00
Karl-Philipp Wulfert
b7d092bf94 0.8.0 2019-05-14 16:25:48 +02:00
Karl-Philipp Wulfert
0f056cb650 ci: adjust jobs to match configuration 2019-05-14 16:25:32 +02:00
Karl-Philipp Wulfert
d8cdce680d build: update dependencies 2019-05-14 16:25:20 +02:00
Rainer Killinger
6822e92749 docs: update README.md. Include e2e command usage 2019-05-10 12:59:20 +02:00
Rainer Killinger
3ed681d444 test: add tests for new e2e command 2019-05-10 12:59:20 +02:00
Rainer Killinger
e1313b55ff feat: add e2e command. Indexes core test files. 2019-05-10 12:59:15 +02:00
Roman Klopsch
f417525195 docs: Update README.md 2019-04-17 08:56:24 +00:00
Karl-Philipp Wulfert
cbc5a57cae docs: update changelog 2019-04-16 15:33:46 +02:00
Karl-Philipp Wulfert
e7fb36c190 0.7.0 2019-04-16 15:33:42 +02:00
Karl-Philipp Wulfert
075c4e7205 build: adjust scripts and contributors 2019-04-16 14:53:28 +02:00
Karl-Philipp Wulfert
9e28ddb313 build: overwrite with suggested files 2019-04-16 14:52:52 +02:00
Karl-Philipp Wulfert
e431098395 build: exclude documentation from packages
Fixes #11
2019-04-16 14:38:07 +02:00
Karl-Philipp Wulfert
c1ad32a527 build: update dependencies 2019-04-16 14:37:52 +02:00
Karl-Philipp Wulfert
89232d9de8 docs: update changelog 2019-04-09 15:59:25 +02:00
Karl-Philipp Wulfert
de3cbdb60a 0.6.0 2019-04-09 15:58:56 +02:00
Karl-Philipp Wulfert
497db2cd07 build: update dependencies
Fixes #7
2019-04-08 11:10:00 +02:00
Karl-Philipp Wulfert
fa66a3abc1 docs: update changelog 2019-04-04 15:18:21 +02:00
Karl-Philipp Wulfert
e4b97151ae 0.5.0 2019-04-04 15:18:18 +02:00
Karl-Philipp Wulfert
1b5dbdb8bc build: adjust npm scripts 2019-04-04 14:24:32 +02:00
Karl-Philipp Wulfert
385617fe39 ci: allow test job to fail 2019-04-04 14:24:32 +02:00
Karl-Philipp Wulfert
9cf6fde050 feat: add method to remove references from a thing
Fixes #6
2019-04-04 14:23:40 +02:00
Karl-Philipp Wulfert
a3b16b8a37 refactor: adjust to changed dependency 2019-04-04 14:23:40 +02:00
Karl-Philipp Wulfert
eed01d6d3d build: update dependencies 2019-04-04 14:23:38 +02:00
Jovan Krunić
184f6dee77 docs: update changelog 2019-03-15 11:40:37 +01:00
Jovan Krunić
923a26ef05 0.4.1 2019-03-15 11:40:02 +01:00
Jovan Krunić
7f8c9eda2f fix: adjust compile npm script to exact configuration guideline 2019-03-15 11:37:27 +01:00
Jovan Krunić
fba64a4ef2 docs: update changelog 2019-03-15 11:32:08 +01:00
Jovan Krunić
e98c938ba8 0.4.0 2019-03-15 11:31:51 +01:00
Jovan Krunić
782fdbebba fix: apply changes suggested by @openstapps/configuration 2019-03-14 14:48:54 +01:00
Jovan Krunić
2692263533 build: update dependencies 2019-03-14 14:36:53 +01:00
Karl-Philipp Wulfert
04fabac8ea ci: remove npm install before script
Fixes #5
2019-02-27 13:43:44 +01:00
Karl-Philipp Wulfert
47e9d0f570 docs: update changelog 2019-02-21 17:23:26 +01:00
Karl-Philipp Wulfert
1a76c98300 0.3.0 2019-02-21 17:23:22 +01:00
Karl-Philipp Wulfert
c74b7c639b build: make sure that dependencies are installed 2019-02-21 17:22:58 +01:00
Karl-Philipp Wulfert
1fe63c133d build: update dependencies 2019-02-21 17:22:43 +01:00
Karl-Philipp Wulfert
33dae87a49 ci: ensure that dependencies are installed 2019-02-18 13:07:04 +01:00
Karl-Philipp Wulfert
9ade1da6db docs: update changelog 2019-02-18 12:59:09 +01:00
Karl-Philipp Wulfert
8fd916c857 0.2.0 2019-02-18 12:59:05 +01:00
Karl-Philipp Wulfert
dd6a09a4c4 build: update dependencies
Set peer dependency core to v0.10.0.
2019-02-18 12:57:12 +01:00
Jovan Krunić
cb70ac5354 docs: update changelog 2019-02-07 17:25:10 +01:00
32 changed files with 6214 additions and 3147 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -1,4 +1,4 @@
FROM node:lts-alpine
FROM registry.gitlab.com/openstapps/projectmanagement/node
ADD . /app
WORKDIR /app

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}

View File

@@ -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,
});
}, {});
}
}

View File

@@ -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();
}

View File

@@ -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
View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
View 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;
}

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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
View 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
View 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;
}
}

View File

@@ -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(),

View File

@@ -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()),
});

View File

@@ -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,

View File

@@ -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
View 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);
}
}

View File

@@ -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();

View File

@@ -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
View 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()}`),
});
}
}

View 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;
}

View 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
View 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();
}
}