diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c0b9b72b..20f80188 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,25 +1,44 @@
image: docker:19.03.5-dind
stages:
- - build
- test
+ - build
+ - integration-test
- push-commit
- push-branch # To avoid duplicating the upload do it sequentially
+ - push-extras
variables:
- DOCKER_IMAGE_FRONTEND: plaza-core-frontend
- DOCKER_IMAGE_BACKEND: plaza-core-backend
- DOCKER_IMAGE_BACKEND_OPTIMIZED: plaza-core-backend-optimized
+ DOCKER_IMAGE_FRONTEND: programaker-core-frontend
+ DOCKER_IMAGE_BACKEND: programaker-core-backend
+ DOCKER_IMAGE_BACKEND_OPTIMIZED: programaker-core-backend-optimized
services:
- docker:19.03.5-dind
## Frontend
-build-frontend:
+test-frontend-logic:
+ stage: test
+ before_script:
+ - cd frontend
+ script:
+ - docker build --target builder --build-arg BUILD_COMMAND=test-logic -f scripts/ci-partial.dockerfile .
+
+test-frontend-on-browser:
+ stage: test
+ before_script:
+ - cd frontend
+ script:
+ - docker build -t browser-test -f scripts/browser-test-ci-partial.dockerfile .
+ - docker run -v `pwd`:/app --rm -e BROWSER_BIN="chromium-browser" -e BROWSER_OPTS="--no-sandbox" browser-test sh -- /app/scripts/run-browser-tests.sh
+
+
+build-prod-frontend:
stage: build
before_script:
- mkdir -p tmp_docker_images/ || true
- cd frontend
+ needs: []
artifacts:
paths:
- tmp_docker_images/
@@ -28,13 +47,14 @@ build-frontend:
- docker build -t $DOCKER_IMAGE_FRONTEND -f scripts/ci-partial.dockerfile .
- docker save $DOCKER_IMAGE_FRONTEND -o ../tmp_docker_images/frontend.tar
-# On frontend there's NO NEED for optimizing the image
-# as it's minified by default (while there are no tests)
-
push-frontend:
stage: push-commit
+ needs:
+ - build-prod-frontend
+ - test-frontend-logic
+ - test-frontend-on-browser
dependencies:
- - build-frontend
+ - build-prod-frontend
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker load -i tmp_docker_images/frontend.tar
@@ -48,8 +68,12 @@ push-frontend:
tag-frontend-branch:
stage: push-branch
+ needs:
+ - build-prod-frontend
+ - test-frontend-logic
+ - test-frontend-on-browser
dependencies:
- - build-frontend
+ - build-prod-frontend
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker load -i tmp_docker_images/frontend.tar
@@ -61,26 +85,67 @@ tag-frontend-branch:
- develop
- master
-## Frontend
+## SSR Frontend
+build-tag-push-ssr-frontend:
+ stage: push-extras
+ dependencies:
+ - test-ssr-frontend
+ - test-frontend-logic
+ - test-frontend-on-browser
+ needs: []
+ script:
+ # Build
+ - docker build --build-arg BUILD_COMMAND=build:ssr -t $DOCKER_IMAGE_FRONTEND -f scripts/ci-partial-ssr.dockerfile .
+ # Push as commit
+ - docker tag $DOCKER_IMAGE_FRONTEND "$CI_REGISTRY"/"$CI_PROJECT_PATH"/frontend-ssr:$CI_COMMIT_SHA
+ - docker push "$CI_REGISTRY"/"$CI_PROJECT_PATH"/frontend-ssr:$CI_COMMIT_SHA
+ # Push as branch
+ - docker tag $DOCKER_IMAGE_FRONTEND "$CI_REGISTRY"/"$CI_PROJECT_PATH"/frontend-ssr:$CI_COMMIT_REF_NAME
+ - docker push "$CI_REGISTRY"/"$CI_PROJECT_PATH"/frontend-ssr:$CI_COMMIT_REF_NAME
+ only:
+ - develop
+ - master
+ when: manual
+
+## Programaker Frontend
build-programaker-frontend:
stage: build
before_script:
- mkdir -p tmp_docker_images/ || true
- cd frontend
+ needs: []
artifacts:
paths:
- tmp_docker_images/
expire_in: 24h # No need to keep it around
script:
- - docker build --build-arg BUILD_COMMAND=build-programaker -t $DOCKER_IMAGE_FRONTEND -f scripts/ci-partial.dockerfile .
+ - docker build --build-arg BUILD_COMMAND=build:programaker-ssr -t $DOCKER_IMAGE_FRONTEND -f scripts/ci-partial-ssr.dockerfile .
- docker save $DOCKER_IMAGE_FRONTEND -o ../tmp_docker_images/programaker-frontend.tar
-# On frontend there's NO NEED for optimizing the image
-# as it's minified by default (while there are no tests)
+test-ssr-frontend:
+ stage: integration-test
+ dependencies:
+ - build-programaker-frontend
+ - build-backend
+ needs:
+ - build-programaker-frontend
+ - build-backend
+ before_script:
+ - docker load -i tmp_docker_images/programaker-frontend.tar
+ - docker load -i tmp_docker_images_optimized/backend-optimized.tar
+ script:
+ - cd utils/integration-tests/ssr-auth-handling
+ - sh install-deps.alpine.sh
+ - CI_TYPE=gitlab bash test-ssr-auth-handling.sh "$DOCKER_IMAGE_BACKEND_OPTIMIZED" "$DOCKER_IMAGE_FRONTEND"
+
push-programaker-frontend:
stage: push-commit
dependencies:
- build-programaker-frontend
+ needs:
+ - build-programaker-frontend
+ - test-frontend-logic
+ - test-frontend-on-browser
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker load -i tmp_docker_images/programaker-frontend.tar
@@ -96,6 +161,10 @@ tag-programaker-frontend-branch:
stage: push-branch
dependencies:
- build-programaker-frontend
+ needs:
+ - build-programaker-frontend
+ - test-frontend-logic
+ - test-frontend-on-browser
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker load -i tmp_docker_images/programaker-frontend.tar
@@ -108,65 +177,64 @@ tag-programaker-frontend-branch:
- master
## Backend
-build-backend:
- stage: build
- artifacts:
- paths:
- - tmp_docker_images/
- expire_in: 24h # No need to keep it around
- before_script:
- - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- - mkdir -p tmp_docker_images/ || true
- - cd backend
- script:
- - docker build -t $DOCKER_IMAGE_BACKEND -f scripts/ci-partial.dockerfile .
- - docker save $DOCKER_IMAGE_BACKEND -o ../tmp_docker_images/backend.tar
-
test-backend:
stage: test
- dependencies:
- - build-backend
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- - docker load -i tmp_docker_images/backend.tar
- cd backend
script:
+ - docker build -t $DOCKER_IMAGE_BACKEND -f scripts/ci-partial.dockerfile .
- docker run -t --rm $DOCKER_IMAGE_BACKEND rebar3 eunit
dialyze-backend:
stage: test
- dependencies:
- - build-backend
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- - docker load -i tmp_docker_images/backend.tar
- cd backend
script:
+ - docker build -t $DOCKER_IMAGE_BACKEND -f scripts/ci-partial.dockerfile .
- docker run -t --rm $DOCKER_IMAGE_BACKEND rebar3 dialyzer
-optimize-image-backend:
- stage: test
- dependencies:
- - build-backend
+build-backend:
+ stage: build
+ needs: []
artifacts:
paths:
- - tmp_docker_images_optimized/ # Don't carry the unoptimized around
+ - tmp_docker_images_optimized/
expire_in: 24h # No need to keep it around
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- - docker load -i tmp_docker_images/backend.tar
- mkdir -p tmp_docker_images_optimized/ || true
- cd backend
script:
+ - docker build -t $DOCKER_IMAGE_BACKEND -f scripts/ci-partial.dockerfile .
- sh ../utils/ci-preparations/optimize-backend-image.sh $DOCKER_IMAGE_BACKEND $DOCKER_IMAGE_BACKEND_OPTIMIZED
# Run sanity check
- docker run -t --rm $DOCKER_IMAGE_BACKEND_OPTIMIZED /app/release/bin/automate escript ../scripts/sanity_check.erl
- docker save $DOCKER_IMAGE_BACKEND_OPTIMIZED -o ../tmp_docker_images_optimized/backend-optimized.tar
+api-test-backend:
+ stage: integration-test
+ dependencies:
+ - build-backend
+ needs:
+ - build-backend
+ before_script:
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - docker load -i tmp_docker_images_optimized/backend-optimized.tar
+ script:
+ - apk add --no-cache curl py3-pip graphviz-dev gcc musl-dev
+ - CI_TYPE=gitlab sh utils/testing/run-api-test.sh $DOCKER_IMAGE_BACKEND_OPTIMIZED
+
push-backend:
stage: push-commit
dependencies:
- - optimize-image-backend
+ - build-backend
+ needs:
+ - build-backend
+ - api-test-backend
+ - test-backend
+ - dialyze-backend
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker load -i tmp_docker_images_optimized/backend-optimized.tar
@@ -181,7 +249,12 @@ push-backend:
tag-backend-branch:
stage: push-branch
dependencies:
- - optimize-image-backend
+ - build-backend
+ needs:
+ - build-backend
+ - api-test-backend
+ - test-backend
+ - dialyze-backend
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker load -i tmp_docker_images_optimized/backend-optimized.tar
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 63bad483..94e810fc 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -23,5 +23,5 @@
(Paste any relevant logs - please use code blocks (```) to format console output, logs, and code as it's very hard to read otherwise.)
-/label ~type:bug ~needs-investigation
+/label ~"type:bug" ~"needs-investigation"
/cc @kenkeiras
diff --git a/.gitlab/issue_templates/New Feature.md b/.gitlab/issue_templates/New Feature.md
index f6c3b65d..f42f72f2 100644
--- a/.gitlab/issue_templates/New Feature.md
+++ b/.gitlab/issue_templates/New Feature.md
@@ -14,5 +14,5 @@
-/label ~type:new-functionality ~needs-investigation
+/label ~"type:new-functionality" ~"needs-investigation"
/cc @kenkeiras
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c4b8edea..8f6bf64a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-**Welcome to Plaza!** We're happy that you are considering contributing, only that way we can drive this project forwards. Let's check some things that you might want to consider.
+**Welcome to Programaker!** We're happy that you are considering contributing, only that way we can drive this project forwards. Let's check some things that you might want to consider.
When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. This way we make sure everyone's effort fit on the general evolution of the project.
@@ -9,7 +9,7 @@ Of course, you can ignore this and just send a *Merge Request* right away for tr
## How to open an Issue
- 1. Go to [the project's issue tracker](https://gitlab.com/plaza-project/plaza-core/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
+ 1. Go to [the project's issue tracker](https://gitlab.com/programaker-project/programaker-core/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
2. Select the `Bug` template for bug reports or `New Feature` for feature proposals.
3. Give the issue an appropriate title.
4. Fill the sections on the issue description.
@@ -19,10 +19,10 @@ Of course, you can ignore this and just send a *Merge Request* right away for tr
## How to develop a new feature or fix
1. Open an issue proposing the feature/fix. Alternatively send a comment to one where it's proposed but no one assigned asking for clarifications.
- 2. After a goal or design has been agreed, [fork the repository](https://gitlab.com/plaza-project/plaza-core/-/forks/new). You can `git clone` your fork and develop on there. Consider creating a new branch on your local clone for this development, if you were developing a "Dark mode theme", you could create a new branch with ` git checkout -b dark-mode-theme `.
+ 2. After a goal or design has been agreed, [fork the repository](https://gitlab.com/programaker-project/programaker-core/-/forks/new). You can `git clone` your fork and develop on there. Consider creating a new branch on your local clone for this development, if you were developing a "Dark mode theme", you could create a new branch with ` git checkout -b dark-mode-theme `.
3. Develop the feature or fix.
4. After the development is completed, commit and push the changes.
- 5. Open a [Merge Request](https://gitlab.com/plaza-project/plaza-core/merge_requests/new), select your repository and branch on the **source branch** column and `plaza-project/plaza-core` and `develop` on the **target branch**.
+ 5. Open a [Merge Request](https://gitlab.com/programaker-project/programaker-core/merge_requests/new), select your repository and branch on the **source branch** column and `programaker-project/programaker-core` and `develop` on the **target branch**.
With these, your feature or fix is sent for review. We'll do our best to answer as soon as possible, but keep in mind that in some cases it might take some time.
diff --git a/LICENSE b/LICENSE
index b7ee289d..cc636264 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2019 PlazaProject
+ Copyright 2019 ProgramakerProject
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 565342cd..aede6f1b 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
-# What is Plaza?
+# What is Programaker?
-Plaza is the project behind [PrograMaker](https://programaker.com). It has the goal of enabling anyone to build anything, without the need for code, servers or technical expertise.
+Programaker is the project behind [PrograMaker.com](https://programaker.com). It has the goal of enabling anyone to build anything, without the need for code, servers or technical expertise.
-Plaza programs are not run on your computer. Thus, it is especially suited for simple tasks that don't require a lot of computing power but that must run contiguously, for example:
+Programaker's programs are not run on your computer. Thus, it is especially suited for simple tasks that don't require a lot of computing power but that must run contiguously, for example:
* Chat bots
* Connections between services
* Scheduled tasks
-Plaza is programmed using MIT's Scratch language. Through it, and Plaza's distributed computer, the steps to create a new program are:
+Programaker can be programmed using MIT's Scratch language (more in progress). Through it, and Programaker's distributed computer, the steps to create a new program are:
* Open a new program in your web browser
* Configure the program steps
* Press run
@@ -16,20 +16,20 @@ Plaza is programmed using MIT's Scratch language. Through it, and Plaza's distri
## Bridges
-Plaza bridges are the components that connect the Plaza platform with external services and devices. This is a list of some bridges in no particular order.
+Programaker bridges are the components that connect the Programaker platform with external services and devices. This is a list of some bridges in no particular order.
| Name | Maturity | Language | Description | License |
|-------------------------------------------------------------------------------------|---------------------|-------------|----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
| Hue lights [[repo](https://gitlab.com/adri1177/hue-lights-bridge)] | Usable/Experimental | Python | Bridge for Phillips Hue lights | [Apache License 2.0](https://gitlab.com/adri1177/hue-lights-bridge/blob/master/LICENSE) |
-| AEMET [[repo](https://gitlab.com/plaza-project/bridges/aemet-bridge)] | Usable/Experimental | Python | Bridge for Spanish Weather Agency predictions. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/aemet-bridge/blob/master/LICENSE) |
-| Meteogalicia [[repo](https://gitlab.com/plaza-project/bridges/meteogalicia-bridge)] | Usable/Experimental | Python | Bridge for Galician weather predictions. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/meteogalicia-bridge/blob/master/LICENSE) |
-| Twitter bridge [[repo](https://gitlab.com/plaza-project/bridges/twitter-bridge)] | In development | Python | Bridge to read data from Twitter. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/twitter-bridge/blob/master/LICENSE) |
-| Toggl bridge [[repo](https://gitlab.com/plaza-project/bridges/toggl-bridge)] | Usable/Experimental | Python | Bridge to keep track of time on Toggl platform. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/toggl-bridge/blob/master/LICENSE) |
-| Telegram bridge [[repo](https://gitlab.com/plaza-project/bridges/telegram-bridge)] | Usable/Experimental | Python | Bridge to control bots on the Telegram IM network. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/telegram-bridge/blob/develop/LICENSE) |
-| Unix bridge [[repo](https://gitlab.com/plaza-project/bridges/unix-bridge)] | Experimental | Python/Bash | Library to write bridges using Unix tools (like bash scripts). | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/unix-bridge/blob/master/LICENSE) |
-| Matrix bridge [[repo](https://gitlab.com/plaza-project/bridges/matrix-bridge)] | Usable/Experimental | Python | Bridge for the Matrix IM network. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/matrix-bridge/blob/master/LICENSE) |
-| XMPP bridge [[repo](https://gitlab.com/plaza-project/bridges/xmpp-bridge)] | Experimental | Python | Bridge for the XMPP/Jabber IM network. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/xmpp-bridge/blob/master/LICENSE) |
-| Gitlab bridge [[repo](https://gitlab.com/plaza-project/bridges/gitlab-bridge)] | Experimental | Python | Bridge for the Gitlab plaform. | [Apache License 2.0](https://gitlab.com/plaza-project/bridges/gitlab-bridge/blob/master/LICENSE) |
+| AEMET [[repo](https://gitlab.com/programaker-project/bridges/aemet-bridge)] | Usable/Experimental | Python | Bridge for Spanish Weather Agency predictions. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/aemet-bridge/blob/master/LICENSE) |
+| Meteogalicia [[repo](https://gitlab.com/programaker-project/bridges/meteogalicia-bridge)] | Usable/Experimental | Python | Bridge for Galician weather predictions. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/meteogalicia-bridge/blob/master/LICENSE) |
+| Twitter bridge [[repo](https://gitlab.com/programaker-project/bridges/twitter-bridge)] | In development | Python | Bridge to read data from Twitter. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/twitter-bridge/blob/master/LICENSE) |
+| Toggl bridge [[repo](https://gitlab.com/programaker-project/bridges/toggl-bridge)] | Usable/Experimental | Python | Bridge to keep track of time on Toggl platform. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/toggl-bridge/blob/master/LICENSE) |
+| Telegram bridge [[repo](https://gitlab.com/programaker-project/bridges/telegram-bridge)] | Usable/Experimental | Python | Bridge to control bots on the Telegram IM network. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/telegram-bridge/blob/develop/LICENSE) |
+| Unix bridge [[repo](https://gitlab.com/programaker-project/bridges/unix-bridge)] | Experimental | Python/Bash | Library to write bridges using Unix tools (like bash scripts). | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/unix-bridge/blob/master/LICENSE) |
+| Matrix bridge [[repo](https://gitlab.com/programaker-project/bridges/matrix-bridge)] | Usable/Experimental | Python | Bridge for the Matrix IM network. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/matrix-bridge/blob/master/LICENSE) |
+| XMPP bridge [[repo](https://gitlab.com/programaker-project/bridges/xmpp-bridge)] | Experimental | Python | Bridge for the XMPP/Jabber IM network. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/xmpp-bridge/blob/master/LICENSE) |
+| Gitlab bridge [[repo](https://gitlab.com/programaker-project/bridges/gitlab-bridge)] | Experimental | Python | Bridge for the Gitlab plaform. | [Apache License 2.0](https://gitlab.com/programaker-project/bridges/gitlab-bridge/blob/master/LICENSE) |
| InfluxDB bridge [[repo](https://gitlab.com/kenkeiras/influxdb-bridge)] | Usable/Experimental | Python | Bridge for the InfluxDB time series database. | [Apache License 2.0](https://gitlab.com/kenkeiras/influxdb-bridge/blob/master/LICENSE) |
## Setup
@@ -58,13 +58,14 @@ An updated version of [erlang](http://www.erlang.org/) and [rebar3](http://www.r
After getting them do the following:
* Go to the backend directory: `cd backend`
+* Get dependencies: `sh ./get-deps.sh`
* Run a rebar shell (which includes a server): `rebar3 shell`
After this, the backend is available on http://localhost:8888 (although the operation is done normaly through the frontend).
#### Docker compose
-A [docker-compose](https://docs.docker.com/compose/overview/) script exists to setup a base deployment of Plaza.
+A [docker-compose.yml](https://docs.docker.com/compose/overview/) script exists to setup a base deployment of Programaker.
This can be used to do some tests or as a help to develop bridges.
But keep in mind that a deployment launched with this script **has no redundancy** and **the data is not saved** between executions.
diff --git a/addons/.gitignore b/addons/.gitignore
index 48a19214..2f74ec65 100644
--- a/addons/.gitignore
+++ b/addons/.gitignore
@@ -1,7 +1,7 @@
node_modules
background.js
background.js.map
-plaza.js
-plaza.js.map
+programaker.js
+programaker.js.map
popup/injected.js
popup/injected.js.map
diff --git a/addons/Dockerfile b/addons/Dockerfile
index 82d5d6f9..e6a6753d 100644
--- a/addons/Dockerfile
+++ b/addons/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:alpine as plaza-addon-ci-base
+FROM node:alpine as programaker-addon-ci-base
RUN apk add make zip
RUN mkdir /app
WORKDIR /app
@@ -9,5 +9,5 @@ ADD package-lock.json /app
RUN npm install .
# Build final app
-FROM plaza-addon-ci-base as builder
+FROM programaker-addon-ci-base as builder
ENTRYPOINT make
diff --git a/addons/Makefile b/addons/Makefile
index 4e45331c..476976ba 100644
--- a/addons/Makefile
+++ b/addons/Makefile
@@ -1,20 +1,20 @@
.PHONY: all build
-DOCKER_IMAGE ?= plaza-addon-builder
-TS = src/Background.ts src/BrowserApi.ts src/Injected.ts src/PlazaApi.ts \
- src/PlazaApi.types.ts src/Popup.ts src/Storage.ts
+DOCKER_IMAGE ?= programaker-addon-builder
+TS = src/Background.ts src/BrowserApi.ts src/Injected.ts src/ProgramakerApi.ts \
+ src/ProgramakerApi.types.ts src/Popup.ts src/Storage.ts
TSC_BUNDLE = node_modules/typescript-bundle
all: build
-build: dist/plaza.xpi
+build: dist/programaker.xpi
docker-build:
docker build -t $(DOCKER_IMAGE) .
docker run -i --rm -v `pwd`:/app $(DOCKER_IMAGE)
-dist/plaza.xpi: background.js popup/injected.js popup/plaza.js manifest.json popup/plaza.html popup/plaza.css icons/icon-48.png
+dist/programaker.xpi: background.js popup/injected.js popup/programaker.js manifest.json popup/programaker.html popup/programaker.css icons/icon-48.png
zip $@ $+
background.js: $(TS) $(TSC_BUNDLE)
@@ -23,7 +23,7 @@ background.js: $(TS) $(TSC_BUNDLE)
popup/injected.js: $(TS) $(TSC_BUNDLE)
node $(TSC_BUNDLE) --project tsconfig_injected.json
-popup/plaza.js: $(TS) $(TSC_BUNDLE)
+popup/programaker.js: $(TS) $(TSC_BUNDLE)
node $(TSC_BUNDLE) --project tsconfig_popup.json
$(TSC_BUNDLE):
diff --git a/addons/manifest.json b/addons/manifest.json
index 273b82bd..bd191fd9 100644
--- a/addons/manifest.json
+++ b/addons/manifest.json
@@ -1,9 +1,9 @@
{
"manifest_version": 2,
- "name": "Plaza",
+ "name": "Programaker",
"version": "0.0.1",
- "description": "Companion addon for plaza.",
+ "description": "Companion addon for programaker.",
"icons": {
"48": "icons/icon-48.png"
@@ -23,13 +23,13 @@
"default_icon": {
"48": "icons/icon-48.png"
},
- "default_title": "Plaza",
- "default_popup": "popup/plaza.html"
+ "default_title": "Programaker",
+ "default_popup": "popup/programaker.html"
},
"applications": {
"gecko": {
- "id": "plaza@plaza.spiral.systems"
+ "id": "contact@programaker.com"
}
}
}
diff --git a/addons/package-lock.json b/addons/package-lock.json
index 2e20288f..83e5c608 100644
--- a/addons/package-lock.json
+++ b/addons/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "plaza-addon",
+ "name": "programaker-addon",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
@@ -213,9 +213,9 @@
"dev": true
},
"js-yaml": {
- "version": "3.12.2",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz",
- "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==",
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
diff --git a/addons/package.json b/addons/package.json
index 5d6661b9..73353494 100644
--- a/addons/package.json
+++ b/addons/package.json
@@ -1,8 +1,8 @@
{
- "name": "plaza-addon",
+ "name": "programaker-addon",
"version": "0.0.1",
"license": "Apache-2.0",
- "description": "Companion addon for plaza.",
+ "description": "Companion addon for programaker.",
"main": "background.js",
"scripts": {
"build": "tsc-bundle --project tsconfig_background.json && tsc-bundle --project tsconfig_popup.json && tsc-bundle --project tsconfig_injected.json",
diff --git a/addons/popup/plaza.css b/addons/popup/programaker.css
similarity index 100%
rename from addons/popup/plaza.css
rename to addons/popup/programaker.css
diff --git a/addons/popup/plaza.html b/addons/popup/programaker.html
similarity index 92%
rename from addons/popup/plaza.html
rename to addons/popup/programaker.html
index 317ce18c..1e21d1c5 100644
--- a/addons/popup/plaza.html
+++ b/addons/popup/programaker.html
@@ -2,7 +2,7 @@
-
+
@@ -35,6 +35,6 @@
Select something and open this again...
-
+
diff --git a/addons/src/Background.ts b/addons/src/Background.ts
index 9e332a05..f1c8c316 100644
--- a/addons/src/Background.ts
+++ b/addons/src/Background.ts
@@ -1,5 +1,5 @@
import { Browser } from "./BrowserApi";
-import * as PlazaApi from "./PlazaApi";
+import * as ProgramakerApi from "./ProgramakerApi";
Browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.command === "addMonitor") {
@@ -7,6 +7,6 @@ Browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
const payload = message.message;
const username = message.username;
- PlazaApi.send_xpath_monitor(username, token, payload);
+ ProgramakerApi.send_xpath_monitor(username, token, payload);
}
});
diff --git a/addons/src/Injected.ts b/addons/src/Injected.ts
index 34da5699..80c17b55 100644
--- a/addons/src/Injected.ts
+++ b/addons/src/Injected.ts
@@ -1,4 +1,4 @@
-import { IXPathDescriptor } from "./PlazaApi.types";
+import { IXPathDescriptor } from "./ProgramakerApi.types";
import { Browser } from "./BrowserApi";
function build_xpath(node: HTMLElement): string {
@@ -244,7 +244,7 @@ function draw_selector_screen(): ISelectorScreenGui {
nameField.placeholder = "Insert monitor name";
nameField.type = "text";
nameField.style.display = "none";
- nameField.id = "plazaAddonNameField";
+ nameField.id = "programakerAddonNameField";
nameLabel.htmlFor = nameField.id;
@@ -278,7 +278,7 @@ try {
selectorScreen.draw();
Browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
- const scriptOptions = message.plazaInjectedOptions;
+ const scriptOptions = message.programakerInjectedOptions;
if (scriptOptions) {
selectorScreen.set_options(scriptOptions);
}
diff --git a/addons/src/Popup.ts b/addons/src/Popup.ts
index e7ccae41..506aa25e 100644
--- a/addons/src/Popup.ts
+++ b/addons/src/Popup.ts
@@ -1,5 +1,5 @@
import * as BrowserApi from "./BrowserApi";
-import * as PlazaApi from "./PlazaApi";
+import * as ProgramakerApi from "./ProgramakerApi";
import * as Storage from "./Storage";
function login() {
@@ -7,7 +7,7 @@ function login() {
const password = (document.querySelector('input[name="login_password"]') as HTMLInputElement).value;
console.log("Loging in:", username);
- PlazaApi.get_token(username, password)
+ ProgramakerApi.get_token(username, password)
.then((token) => Storage.save_auth_token(username, token))
.then(() => show_ready());
@@ -48,7 +48,7 @@ function check_token() {
BrowserApi.get_current_tab()
.then((tab) => {
BrowserApi.run_on_tab(tab, "/popup/injected.js", () => {
- BrowserApi.send_message_to_tab(tab, {plazaInjectedOptions: { username, token }});
+ BrowserApi.send_message_to_tab(tab, {programakerInjectedOptions: { username, token }});
BrowserApi.close_popup();
});
}, (error) => {
diff --git a/addons/src/PlazaApi.ts b/addons/src/ProgramakerApi.ts
similarity index 100%
rename from addons/src/PlazaApi.ts
rename to addons/src/ProgramakerApi.ts
diff --git a/addons/src/PlazaApi.types.ts b/addons/src/ProgramakerApi.types.ts
similarity index 100%
rename from addons/src/PlazaApi.types.ts
rename to addons/src/ProgramakerApi.types.ts
diff --git a/addons/src/Storage.ts b/addons/src/Storage.ts
index 8b179309..8c7dc78c 100644
--- a/addons/src/Storage.ts
+++ b/addons/src/Storage.ts
@@ -1,4 +1,4 @@
-const DB_NAME = "PlazaDB";
+const DB_NAME = "ProgramakerDB";
const DB_VERSION = 1;
const AUTH_TOKEN_STORE = "auth_token";
diff --git a/addons/tsconfig_popup.json b/addons/tsconfig_popup.json
index ec994a74..d2a1566e 100644
--- a/addons/tsconfig_popup.json
+++ b/addons/tsconfig_popup.json
@@ -4,7 +4,7 @@
"./src/Popup.ts"
],
"compilerOptions": {
- "outFile": "./popup/plaza.js",
+ "outFile": "./popup/programaker.js",
"sourceMap": true,
"module": "amd",
"declaration": false,
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 4f315059..af6110df 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,4 +1,4 @@
-FROM erlang:22-alpine as plaza-backend-ci-base
+FROM erlang:23-alpine as programaker-backend-ci-base
# Build dependencies
RUN apk add git gcc libc-dev g++ make libtool autoconf automake
@@ -7,19 +7,19 @@ RUN mkdir /app
WORKDIR /app
# Pre-build dependencies
-ADD rebar.config /app
-RUN rebar3 get-deps && rebar3 compile
+ADD rebar.config get-deps.sh /app/
+RUN sh ./get-deps.sh && rebar3 compile
RUN rebar3 dialyzer
# Add application code
-FROM plaza-backend-ci-base as develop
+FROM programaker-backend-ci-base as develop
ADD . /app
RUN sh -x -c 'if [ ! -f config/sys.config ]; then cp -v config/sys.config.orig config/sys.config ; fi'
# Prepare release
RUN rebar3 release
-FROM alpine as final
+FROM alpine:3.12 as final
RUN apk add ncurses libstdc++ erlang
diff --git a/backend/apps/automate/src/automate.app.src b/backend/apps/automate/src/automate.app.src
index 5553aa28..8f9e477d 100644
--- a/backend/apps/automate/src/automate.app.src
+++ b/backend/apps/automate/src/automate.app.src
@@ -2,13 +2,26 @@
{description, "Auto-mate node."},
{vsn, "0.0.0"},
{registered, []},
+ {mod, { automate_app, [] }},
{applications, [ kernel
, stdlib
- , automate_configuration
- , automate_rest_api
- , automate_bot_engine
- , automate_monitor_engine
+ , cowboy
+ , jiffy
+ , uuid
+ , eargon2
+ , mochiweb
+ , mochiweb_xpath
+ , prometheus
+ , qdate
+
]},
+ {included_applications, [ automate_configuration
+ , automate_logging
+ , automate_storage
+ , automate_coordination
+ , automate_engines
+ , automate_rest_api
+ ]},
{env, [
]},
{modules, []},
diff --git a/backend/apps/automate_service_user_registration/src/automate_service_user_registration_app.erl b/backend/apps/automate/src/automate_app.erl
similarity index 67%
rename from backend/apps/automate_service_user_registration/src/automate_service_user_registration_app.erl
rename to backend/apps/automate/src/automate_app.erl
index 55b45f33..ca10f27f 100644
--- a/backend/apps/automate_service_user_registration/src/automate_service_user_registration_app.erl
+++ b/backend/apps/automate/src/automate_app.erl
@@ -1,22 +1,26 @@
%%%-------------------------------------------------------------------
-%% @doc automate_service_registry APP API
+%% @doc automate public API
%% @end
%%%-------------------------------------------------------------------
--module(automate_service_user_registration_app).
+-module(automate_app).
-behaviour(application).
--define(APPLICATION, automate_service_user_registration).
%% Application callbacks
-export([start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
+
start(_StartType, _StartArgs) ->
- ?APPLICATION:start_link().
+ automate_sup:start_link().
%%--------------------------------------------------------------------
stop(_State) ->
ok.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
diff --git a/backend/apps/automate/src/automate_sup.erl b/backend/apps/automate/src/automate_sup.erl
new file mode 100644
index 00000000..00eb7352
--- /dev/null
+++ b/backend/apps/automate/src/automate_sup.erl
@@ -0,0 +1,80 @@
+%%%-------------------------------------------------------------------
+%% @doc automate top level supervisor.
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(automate_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+-define(SERVER, ?MODULE).
+-include("../../automate_common_types/src/definitions.hrl").
+
+%%====================================================================
+%% API functions
+%%====================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+
+%%====================================================================
+%% Supervisor callbacks
+%%====================================================================
+
+%% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules}
+init([]) ->
+ automate_configuration_app:check_assertions(),
+ {ok, { { rest_for_one, ?AUTOMATE_SUPERVISOR_INTENSITY, ?AUTOMATE_SUPERVISOR_PERIOD},
+ [ #{ id => automate_configuration
+ , start => { automate_configuration_distributed, start_link, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_configuration]
+ }
+ , #{ id => automate_logging
+ , start => {automate_logging_app, start, []}
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_storage]
+ }
+ , #{ id => automate_storage
+ , start => {automate_storage, start_link, []}
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_storage]
+ }
+ , #{ id => automate_coordination
+ , start => {automate_coordination, start_link, []}
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_coordination]
+ }
+ , #{ id => automate_engines
+ , start => { automate_engines_app, start, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [ automate_engines ]
+ }
+ , #{ id => automate_rest_api
+ , start => { automate_rest_api_app, start, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [ automate_services ]
+ }
+ ]} }.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine.app.src b/backend/apps/automate_bot_engine/src/automate_bot_engine.app.src
index bb9b9e83..0921fb76 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine.app.src
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine.app.src
@@ -7,11 +7,15 @@
, stdlib
, automate_storage
, automate_coordination
- , automate_services_all
+ , automate_services_time
, automate_configuration
, automate_channel_engine
- %% , automate_program_linker %% Not an application, just a module
+ , automate_service_registry
+ , automate_service_port_engine
]},
+ { included_applications, [ automate_program_linker %% Not a real application, just a module
+ , automate_testing
+ ] },
{env, [
]},
{modules, []},
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine.erl
index 55e117c9..6eb0a3c9 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine.erl
@@ -6,26 +6,28 @@
-module(automate_bot_engine).
%% Application callbacks
--export([ stop_program_threads/2
- , change_program_status/3
+-export([ stop_program_threads/1
+ , change_program_status/2
, get_user_from_pid/1
+ , get_bridges_on_program/1
+ , get_user_generated_logs/1
]).
--spec stop_program_threads(binary(),binary()) -> ok | {error, any()}.
-stop_program_threads(_UserId, ProgramId) ->
+-include("../../automate_storage/src/records.hrl").
+
+-spec stop_program_threads(binary()) -> ok.
+stop_program_threads(ProgramId) ->
case automate_storage:get_threads_from_program(ProgramId) of
{ ok, Threads } ->
lists:foreach(fun (ThreadId) ->
automate_bot_engine_thread_runner:stop_by_id(ThreadId)
end, Threads),
- ok;
- { error, Reason } ->
- { error, Reason }
+ ok
end.
--spec change_program_status(binary(),binary(),boolean()) -> ok | {error, any()}.
-change_program_status(Username, ProgramId, Status) ->
- case automate_storage:update_program_status(Username, ProgramId, Status) of
+-spec change_program_status(binary(),boolean()) -> ok | {error, any()}.
+change_program_status(ProgramId, Status) ->
+ case automate_storage:update_program_status(ProgramId, Status) of
ok ->
ok = automate_bot_engine_launcher:update_program(ProgramId),
ok;
@@ -33,7 +35,14 @@ change_program_status(Username, ProgramId, Status) ->
{ error, Reason }
end.
--spec get_user_from_pid(pid()) -> { ok, binary() } | {error, not_found}.
+-spec get_user_from_pid(pid()) -> { ok, owner_id() } | {error, not_found}.
get_user_from_pid(Pid) ->
automate_storage:get_user_from_pid(Pid).
+-spec get_bridges_on_program(#user_program_entry{}) -> { ok, [binary()] }.
+get_bridges_on_program(Program) ->
+ automate_bot_engine_program_decoder:get_bridges_on_program(Program).
+
+-spec get_user_generated_logs(binary()) -> {error, not_found} | {ok, [#user_generated_log_entry{}]}.
+get_user_generated_logs(Pid) ->
+ automate_storage:get_user_generated_logs(Pid).
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_app.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_app.erl
index 7cb9fc45..30f6ae26 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_app.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_app.erl
@@ -10,11 +10,19 @@
%% Application callbacks
-export([start/2, stop/1]).
+-include("databases.hrl").
+
%%====================================================================
%% API
%%====================================================================
start(_StartType, _StartArgs) ->
+ ok = mnesia:wait_for_tables(?BOT_REQUIRED_DBS, automate_configuration:get_table_wait_time()),
+ case mnesia:wait_for_tables(?BOT_EXTRA_DBS, automate_configuration:get_table_wait_time()) of
+ ok -> ok;
+ Result ->
+ automate_logging:log_platform(error, io_lib:format("Error waiting for extra bot_engine tables: ~p", [Result]))
+ end,
automate_bot_engine_sup:start_link().
%%--------------------------------------------------------------------
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_naive_lists.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_naive_lists.erl
index c2392da5..19e2d655 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_naive_lists.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_naive_lists.erl
@@ -8,6 +8,8 @@
, get_length/1
, contains/2
, get_item_num/2
+
+ , pad_to_length/3
]).
%%%===================================================================
@@ -26,14 +28,17 @@ insert_nth(List, Index, Value) when Index > 0 ->
replace_nth(List, Index, Value) when Index > 0 ->
naive_replace_nth_into_list([], List, Index, Value).
--spec get_nth([any()], pos_integer()) -> {ok, any()} | {error, not_found}.
+-spec get_nth([any()], pos_integer()) -> {ok, any()} | {error, not_found} | {error, invalid_list_index_type}.
get_nth(List, Nth) when is_integer(Nth) and (Nth > 0) ->
case Nth =< length(List) of
true ->
{ok, lists:nth(Nth, List)};
false ->
{error, not_found}
- end.
+ end;
+get_nth(_List, _Nth) ->
+ {error, invalid_list_index_type}.
+
-spec get_length([any()]) -> {ok, non_neg_integer()}.
get_length(List) when is_list(List) ->
@@ -47,6 +52,11 @@ contains(List, Value) ->
-spec get_item_num([any()], any()) -> {ok, pos_integer()} | {error, not_found}.
get_item_num(List, Value) ->
find_item(List, Value, 1).
+
+
+pad_to_length(List, Length, Padding) ->
+ List ++ build_list(Length - length(List), Padding).
+
%%%===================================================================
%%% Internal functions
%%%===================================================================
@@ -94,3 +104,12 @@ find_item([_ | T ], Value, Index) ->
find_item(T, Value, Index + 1).
+-spec build_list(number(), any()) -> [any()].
+build_list(Length, _Element) when Length =< 0 ->
+ [];
+build_list(Length, Element) ->
+ build_list(Length, Element, []).
+build_list(0, _Element, Acc) ->
+ Acc;
+build_list(Length, Element, Acc) when Length > 0 ->
+ build_list(Length - 1, Element, [Element | Acc]).
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_operations.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_operations.erl
index 4ead1582..07f02c5b 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_operations.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_operations.erl
@@ -2,14 +2,23 @@
%% API
-export([ get_expected_signals/1
- , run_thread/2
+ , run_thread/3
, get_result/2
]).
+-ifdef(TEST).
+-export([ run_instruction/3
+ ]).
+-endif.
+
-define(SERVER, ?MODULE).
-include("../../automate_storage/src/records.hrl").
-include("program_records.hrl").
-include("instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+-include("../../automate_logging/src/records.hrl").
+
+-define(UTILS, automate_bot_engine_utils).
%%%===================================================================
%%% API
@@ -18,9 +27,9 @@
get_expected_signals(Threads) ->
{ok, get_expected_signals_from_threads(Threads)}.
--spec get_block_result(map(), #program_thread{}) -> {ok, any()} | {error, not_found}.
+-spec get_result(map(), #program_thread{}) -> {ok, any(), #program_thread{}} | {error, not_found}.
get_result(Operation, Thread) ->
- get_block_result(Operation, Thread).
+ run_getter_block(Operation, Thread).
%%%===================================================================
%%% Internal functions
@@ -38,10 +47,92 @@ get_expected_action_from_thread(Thread) ->
{error, element_not_found} ->
none;
{ok, Operation} ->
- get_expected_action_from_operation(Operation)
+ get_expected_action_from_operation(Operation, Thread)
end.
-get_expected_action_from_operation(_) ->
+get_expected_action_from_operation(#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_BLOCK
+ , ?VALUE := [ Listened=#{ ?TYPE := <<"services.", MonitorPath/binary>>
+ }
+ ]
+ }
+ ]
+ }, #program_thread{ program_id=ProgramId }) ->
+ [ServiceId, MonitorKey] = binary:split(MonitorPath, <<".">>),
+
+ {ok, UserId} = automate_storage:get_program_owner(ProgramId),
+
+ Args = case Listened of
+ #{ ?ARGUMENTS := Arguments } ->
+ Arguments;
+ _ ->
+ []
+ end,
+ case ?UTILS:get_block_key_subkey(Args) of
+ { key_and_subkey, Key, SubKey } ->
+ automate_service_registry_query:listen_service(ServiceId, UserId, { Key, SubKey });
+ { key, Key } ->
+ automate_service_registry_query:listen_service(ServiceId, UserId, { Key, undefined });
+ { not_found } ->
+ automate_service_registry_query:listen_service(ServiceId, UserId, { MonitorKey, undefined })
+ end,
+
+ ?TRIGGERED_BY_MONITOR;
+
+get_expected_action_from_operation(#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := _VarName
+ }
+ ]
+ }, _Thread) ->
+ %% TODO: Have channel to send variable updates
+ ?SIGNAL_PROGRAM_TICK;
+
+get_expected_action_from_operation(#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_BLOCK
+ , ?VALUE := [ #{ ?TYPE := ?WAIT_FOR_MONITOR
+ , ?ARGUMENTS := MonitorArgs=#{ ?FROM_SERVICE := ServiceId }
+ }
+ ]
+ }
+ ]
+ }, #program_thread{ program_id=ProgramId }) ->
+
+ {ok, Owner} = automate_storage:get_program_owner(ProgramId),
+
+ Key = case MonitorArgs of
+ #{ <<"key">> := MonKey } ->
+ MonKey;
+ _ ->
+ undefined
+ end,
+ automate_service_registry_query:listen_service(ServiceId, Owner, {Key, undefined}),
+ ?TRIGGERED_BY_MONITOR;
+
+
+get_expected_action_from_operation(Op=#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ }, _Thread) ->
+ automate_logging:log_platform(error,
+ io_lib:format("Cannot find appropriate channel to hear for op: ~p", [Op])),
+ ?TRIGGERED_BY_MONITOR;
+
+get_expected_action_from_operation(#{ ?TYPE := ?WAIT_FOR_MONITOR
+ , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := #{ ?FROM_SERVICE := ServiceId } }
+ }, #program_thread{ program_id=ProgramId }) ->
+
+ {ok, Owner} = automate_storage:get_program_owner(ProgramId),
+
+ Key = case MonitorArgs of
+ #{ <<"key">> := MonKey } ->
+ MonKey;
+ _ ->
+ undefined
+ end,
+ automate_service_registry_query:listen_service(ServiceId, Owner, {Key, undefined}),
+ ?TRIGGERED_BY_MONITOR;
+
+get_expected_action_from_operation(_Op, _Thread) ->
+ %% Just wait for next TICK
?SIGNAL_PROGRAM_TICK.
-spec get_instruction(#program_thread{}) -> {ok, map()} | {error, element_not_found}.
@@ -71,181 +162,435 @@ resolve_subblock_with_position(#{<<"contents">> := Contents}, [Position | _]) wh
{error, element_not_found};
resolve_subblock_with_position(#{<<"contents">> := Contents}, [Position | T]) ->
- resolve_subblock_with_position(lists:nth(Position, Contents), T).
+ case (Position > 0) and (Position =< length(Contents)) of
+ true -> resolve_subblock_with_position(lists:nth(Position, Contents), T);
+ false ->
+ {error, element_not_found}
+ end.
--spec run_thread(#program_thread{}, {atom(), any()})
+-spec run_thread(#program_thread{}, {atom(), any()}, binary())
-> {stopped, thread_finished} | {did_not_run, waiting}
- | {did_not_run, {new_state, #program_thread{}}}
- | {ran_this_tick, #program_thread{}}.
-run_thread(Thread, Message) ->
+ | {did_not_run, {new_state, #program_thread{}}}
+ | {ran_this_tick, #program_thread{}, [_]}.
+run_thread(Thread=#program_thread{program_id=ProgramId}, Message, ThreadId) ->
case get_instruction(Thread) of
{ok, Instruction} ->
+
+ %% Prepare data to log call and return of the instruction
+ CallStartTime = erlang:system_time(second),
+ OpName = case Instruction of
+ #{ ?TYPE := Operation } -> Operation;
+ _ -> %% This will normally happen on "content" blocks
+ none
+ end,
+
+ OwnerId = case automate_storage:get_program_owner(ProgramId) of
+ {ok, Owner} ->
+ Owner;
+ {error, Reason} ->
+ io:fwrite("[Double ERROR][ThreadId=~p] Now owner found: ~0tp~n",
+ [ThreadId, Reason]),
+ none
+ end,
+
try
run_instruction(Instruction, Thread, Message)
of
Result ->
+ case Result of
+ { ran_this_tick, _, Arguments } ->
+ case OpName of
+ none -> ok;
+ _ ->
+ CallEndTime = erlang:system_time(second),
+
+ automate_logging:log_program_call_by_user(
+ #call_data{ call_start_time=CallStartTime
+ , call_end_time=CallEndTime
+ , block_id=?UTILS:get_block_id(Instruction)
+ , program_id=ProgramId
+ , thread_id=ThreadId
+ , succeeded=true
+ , operation=OpName
+ , arguments=Arguments
+ , result=ok
+ }, OwnerId)
+ end;
+ _ -> ok
+ end,
+
+ case {Result, Instruction} of
+ { {ran_this_tick, Thread2, _}
+ , #{ ?BLOCK_ID := BlockId
+ , ?REPORT_STATE := true
+ }
+ } ->
+
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+
+ Value = case automate_bot_engine_variables:retrieve_instruction_memory(Thread2, BlockId) of
+ {error, not_found} -> none;
+ {ok, BlockMem} -> BlockMem
+ end,
+
+ #program_thread{ global_memory=Memory } = Thread2,
+
+ %% Trigger element update
+ ok = automate_channel_engine:send_to_channel(ChannelId, #{ <<"key">> => block_run_events
+ , <<"subkey">> => BlockId
+ , <<"value">> => Value
+ , <<"memory">> => Memory
+ } );
+ _ -> ok
+ end,
Result
catch ErrorNS:Error:StackTrace ->
- io:fwrite("[Thread] Critical error: ~p~n~p~n", [{ErrorNS, Error}, StackTrace]),
+ io:fwrite("[ERROR][Thread][ProgId=~p,ThreadId=~p] Critical error: ~0tp~n~0tp~n",
+ [ProgramId, ThreadId, {ErrorNS, Error}, StackTrace]),
+
+ CallEndTime = erlang:system_time(second),
+
+ automate_logging:log_program_call_by_user(
+ #call_data{ call_start_time=CallStartTime
+ , call_end_time=CallEndTime
+ , block_id=?UTILS:get_block_id(Instruction)
+ , program_id=ProgramId
+ , thread_id=ThreadId
+ , succeeded=false
+ , operation=OpName
+ , arguments=[]
+ , result=Error
+ }, OwnerId),
+
+ {EventData, EventMessage, FailedBlockId} =
+ case Error of
+ #program_error{ error=#variable_not_set{variable_name=VariableName}
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Variable '~s' not set", [VariableName]))
+ , BlockId
+ };
+
+ #program_error{ error=#list_not_set{list_name=ListName}
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("List '~s' not set", [ListName]))
+ , BlockId
+ };
+
+ #program_error{error=#index_not_in_list{ list_name=ListName
+ , index=Index
+ , max=MaxIndex
+ }
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Cannot access position ~0tp on list '~s'. Only ~0tp elements",
+ [Index, ListName, MaxIndex]))
+ , BlockId
+ };
+
+ #program_error{error=#invalid_list_index_type{ list_name=ListName
+ , index=Index
+ }
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Trying to access non valid position list '~s'. "
+ "Position must be a non-negative number. Found '~0tp'.",
+ [ListName, Index]))
+ , BlockId
+ };
+
+ #program_error{ error=#memory_item_size_exceeded{next_size=NextSize, max_size=MaxSize}
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Memory item size exceeded. Next size: ~p. Max: ~p", [NextSize, MaxSize]))
+ , BlockId
+ };
+
+ #program_error{error=#disconnected_bridge{ bridge_id=BridgeId
+ , action=Action
+ }
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Cannot run action '~s' on disconnected bridge '~s'.",
+ [Action, BridgeId]))
+ , BlockId
+ };
+
+ #program_error{error=#bridge_call_connection_not_found{ bridge_id=BridgeId
+ , action=Action
+ }
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Cannot run action '~s' on bridge '~s'. No connection found. Report this to the administrator to solve it.",
+ [Action, BridgeId]))
+ , BlockId
+ };
+
+ #program_error{error=#bridge_call_timeout{ bridge_id=BridgeId
+ , action=Action
+ }
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Timeout: Call to action '~s' on bridge '~s' took too long.",
+ [Action, BridgeId]))
+ , BlockId
+ };
+
+ #program_error{error=#bridge_call_failed{ reason=FailReason
+ , bridge_id=BridgeId
+ , action=Action
+ }
+ , block_id=BlockId
+ } ->
+ case FailReason of
+ R when is_binary(R) ->
+ { Error
+ , list_to_binary(io_lib:format("Bridge reported error on action '~s' (bridge id='~s'). Reason: ~s",
+ [Action, BridgeId, R]))
+ , BlockId
+ };
+ _ ->
+ { Error
+ , list_to_binary(io_lib:format("Bridge reported error on action '~s' (bridge id='~s').",
+ [Action, BridgeId]))
+ , BlockId
+ }
+ end;
+
+ #program_error{error=#bridge_call_error_getting_resource{ bridge_id=BridgeId
+ , action=Action
+ }
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Error preparing resourcesn to run action '~s' on bridge '~s'. Report this to the administrator to solve it.",
+ [Action, BridgeId]))
+ , BlockId
+ };
+
+ #program_error{error=#unknown_operation{}
+ , block_id=BlockId
+ } ->
+ { Error
+ , list_to_binary(io_lib:format("Unknown operation found! Please, report this to the administrator",
+ []))
+ , BlockId
+ };
+ _ ->
+ %% Although this might be extracted from the thread's position
+ {Error, <<"Unknown error">>, none}
+ end,
+
+ automate_logging:log_program_error(#user_program_log_entry{ program_id=ProgramId
+ , thread_id=ThreadId
+ , owner=OwnerId
+ , block_id=FailedBlockId
+ , event_data=EventData
+ , event_time=erlang:system_time(millisecond)
+ , event_message=EventMessage
+ , severity=error
+ , exception_data={ErrorNS,Error,StackTrace}
+ }),
{stopped, {ErrorNS, Error}} %% Critical errors trigger a stop
end;
{error, element_not_found} ->
{stopped, thread_finished}
end.
-run_instruction(#{ ?TYPE := ?COMMAND_SET_VARIABLE
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
- , ?VALUE := VariableName
- }
- , ValueArgument
- ]
- }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
-
- {ok, Value} = automate_bot_engine_variables:resolve_argument(ValueArgument, Thread),
- {ok, NewThreadState } = automate_bot_engine_variables:set_program_variable(Thread, VariableName, Value),
- {ran_this_tick, increment_position(NewThreadState)};
-
-
-run_instruction(#{ ?TYPE := ?COMMAND_CHANGE_VARIABLE
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
- , ?VALUE := VariableName
- }
- , ValueArgument
- ]
- }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
-
- {ok, Change} = automate_bot_engine_variables:resolve_argument(ValueArgument, Thread),
- {ok, NewValue} = case automate_bot_engine_variables:get_program_variable(Thread, VariableName) of
+run_instruction(Op=#{ ?TYPE := ?COMMAND_SET_VARIABLE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := VariableName
+ }
+ , ValueArgument
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(ValueArgument, Thread, Op),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, VariableName, Value, ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread2), [VariableName, Value]};
+
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_CHANGE_VARIABLE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := VariableName
+ }
+ , ValueArgument
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, Change, Thread2} = automate_bot_engine_variables:resolve_argument(ValueArgument, Thread, Op),
+ {ok, NewValue} = case automate_bot_engine_variables:get_program_variable(Thread2, VariableName) of
{ok, PrevValue} ->
automate_bot_engine_values:add(PrevValue, Change);
{error, not_found} ->
- {ok, Change}
- end,
- {ok, NewThreadState } = automate_bot_engine_variables:set_program_variable(Thread, VariableName, NewValue),
- {ran_this_tick, increment_position(NewThreadState)};
-
-run_instruction(#{ ?TYPE := ?COMMAND_REPEAT
- , ?ARGUMENTS := [Argument]
- }, Thread=#program_thread{ position=Position }, {?SIGNAL_PROGRAM_TICK, _}) ->
-
- {Times, Value} = case automate_bot_engine_variables:retrieve_instruction_memory(Thread) of
- {ok, MemoryValue} ->
- MemoryValue;
- {error, not_found} ->
- {ok, TimesStr} = automate_bot_engine_variables:resolve_argument(Argument, Thread),
- LoopTimes = to_int(TimesStr),
- {LoopTimes, 0}
+ throw(#program_error{ error=#variable_not_set{ variable_name=VariableName }
+ , block_id=?UTILS:get_block_id(Op)
+ })
end,
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, VariableName, NewValue, ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread2), [VariableName, NewValue]};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_REPEAT
+ , ?ARGUMENTS := [Argument]
+ }, Thread=#program_thread{ position=Position }, {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {Times, Value, Thread3} = case automate_bot_engine_variables:retrieve_instruction_memory(Thread) of
+ {ok, {WasTimes, WasValue}} ->
+ {WasTimes, WasValue, Thread};
+ {error, not_found} ->
+ {ok, TimesStr, Thread2} = automate_bot_engine_variables:resolve_argument(Argument, Thread, Op),
+ LoopTimes = to_int(TimesStr),
+ {LoopTimes, 0, Thread2}
+ end,
+
+ Thread4 = case Op of
+ #{ ?BLOCK_ID := BlockId } ->
+ automate_bot_engine_variables:set_instruction_memory( Thread3
+ , #{ <<"as_list">> => [null, Value + 1] }
+ , BlockId
+ );
+ _ ->
+ Thread3
+ end,
case Value < Times of
true ->
- NextIteration = automate_bot_engine_variables:set_instruction_memory( Thread
- , {Times, Value + 1}
- ),
- {ran_this_tick, NextIteration#program_thread{ position=Position ++ [1] }};
+ Thread5 = automate_bot_engine_variables:set_instruction_memory( Thread4
+ , {Times, Value + 1}
+ ),
+ {ran_this_tick, Thread5#program_thread{ position=Position ++ [1], direction=forward }, [Value]};
false ->
- NextIteration = automate_bot_engine_variables:unset_instruction_memory(Thread),
- {ran_this_tick, increment_position(NextIteration)}
+ %% Note that the value of the block (by id) is NOT removed, so it
+ %% can be used by subsequent blocks.
+ Thread5 = automate_bot_engine_variables:unset_instruction_memory(Thread4),
+ {ran_this_tick, increment_position(Thread5), [Value]}
end;
-run_instruction(#{ ?TYPE := ?COMMAND_REPEAT_UNTIL
- , ?ARGUMENTS := [Argument]
- }, Thread=#program_thread{ position=Position }, {?SIGNAL_PROGRAM_TICK, _}) ->
+run_instruction(Op=#{ ?TYPE := ?COMMAND_REPEAT_UNTIL
+ , ?ARGUMENTS := [Argument]
+ }, Thread=#program_thread{ position=Position }, {?SIGNAL_PROGRAM_TICK, _}) ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(Argument, Thread),
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(Argument, Thread, Op),
case Value of
false ->
- {ran_this_tick, Thread#program_thread{ position=Position ++ [1] }};
+ %% Condition not macthed, going in
+ {ran_this_tick, Thread2#program_thread{ position=Position ++ [1], direction=forward }, [Value]};
_ ->
- {ran_this_tick, increment_position(Thread)}
+ %% Condition Matched, NOT going into the loop
+ {ran_this_tick, increment_position(Thread2), [Value]}
end;
-run_instruction(#{ ?TYPE := ?COMMAND_IF
- , ?ARGUMENTS := [Argument]
- }, Thread=#program_thread{ position=Position }, {?SIGNAL_PROGRAM_TICK, _}) ->
+run_instruction(Op=#{ ?TYPE := ?COMMAND_IF
+ , ?ARGUMENTS := [Argument]
+ }, Thread=#program_thread{ position=Position, direction=Direction }, {?SIGNAL_PROGRAM_TICK, _}) ->
- case automate_bot_engine_variables:retrieve_instruction_memory(Thread) of
- {ok, _} ->
- NextIteration = automate_bot_engine_variables:unset_instruction_memory(Thread),
- {ran_this_tick, increment_position(NextIteration)};
- {error, not_found} ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(
- Argument, Thread),
+ case Direction of
+ up ->
+ %% Coming back from condition
+ {ran_this_tick, increment_position(Thread), [Direction]};
+ forward ->
+ %% Getting into the IF
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(
+ Argument, Thread, Op),
case Value of
- false -> %% Not matching, skipping
- {ran_this_tick, increment_position(Thread)};
- _ -> %% Matching, going in
- NextIteration = automate_bot_engine_variables:set_instruction_memory(
- Thread, {already_run, true}),
- {ran_this_tick, NextIteration#program_thread{ position=Position ++ [1] }}
-
+ false ->
+ %% Not matching, skipping
+ {ran_this_tick, increment_position(Thread2), [Direction, Value]};
+ _ ->
+ %% Matching, going in
+ Thread3 = automate_bot_engine_variables:set_instruction_memory(
+ Thread2, {already_run, true}),
+ {ran_this_tick, Thread3#program_thread{ position=Position ++ [1], direction=forward }, [Direction, Value]}
end
end;
-run_instruction(#{ ?TYPE := ?COMMAND_IF_ELSE
- , ?ARGUMENTS := [Argument]
- }, Thread=#program_thread{ position=Position }, {?SIGNAL_PROGRAM_TICK, _}) ->
+run_instruction(Op=#{ ?TYPE := ?COMMAND_IF_ELSE
+ , ?ARGUMENTS := [Argument]
+ }, Thread=#program_thread{ position=Position, direction=Direction }, {?SIGNAL_PROGRAM_TICK, _}) ->
- case automate_bot_engine_variables:retrieve_instruction_memory(Thread) of
- {ok, _} ->
- NextIteration = automate_bot_engine_variables:unset_instruction_memory(Thread),
- {ran_this_tick, increment_position(NextIteration)};
- {error, not_found} ->
- NextIteration = automate_bot_engine_variables:set_instruction_memory(
- Thread, {already_run, true}),
- {ok, Value} = automate_bot_engine_variables:resolve_argument(Argument, NextIteration),
+ case Direction of
+ up ->
+ %% Coming back from condition
+ {ran_this_tick, increment_position(Thread), [Direction]};
+ forward ->
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(Argument, Thread, Op),
case Value of
- false -> %% Not matching, going for else
- {ran_this_tick, NextIteration#program_thread{ position=Position ++ [2, 1] }};
- _ -> %% Matching, going for if
- {ran_this_tick, NextIteration#program_thread{ position=Position ++ [1, 1] }}
+ false ->
+ %% Not matching, going for else
+ {ran_this_tick, Thread2#program_thread{ position=Position ++ [2, 1], direction=forward }, [Direction, Value]};
+ _ ->
+ %% Matching, going for if
+ {ran_this_tick, Thread2#program_thread{ position=Position ++ [1, 1], direction=forward }, [Direction, Value]}
end
end;
-run_instruction(#{ ?TYPE := ?COMMAND_WAIT_UNTIL
- , ?ARGUMENTS := [Argument]
- }, Thread=#program_thread{}, _) ->
+run_instruction(Op=#{ ?TYPE := ?COMMAND_WAIT_UNTIL
+ , ?ARGUMENTS := [Argument]
+ }, Thread=#program_thread{}, _) ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(Argument, Thread),
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(Argument, Thread, Op),
case Value of
false ->
- {did_not_run, Thread};
+ {did_not_run, waiting};
_ ->
- {ran_this_tick, increment_position(Thread)}
+ {ran_this_tick, increment_position(Thread2), [Value]}
end;
-run_instruction(#{ ?TYPE := ?COMMAND_WAIT
- , ?ARGUMENTS := [Argument]
- }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
+run_instruction(Op=#{ ?TYPE := ?COMMAND_WAIT
+ , ?ARGUMENTS := [Argument]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
- {ok, Seconds} = automate_bot_engine_variables:resolve_argument(Argument, Thread),
- StartTime = case automate_bot_engine_variables:retrieve_instruction_memory(Thread) of
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(Argument, Thread, Op),
+ StartTime = case automate_bot_engine_variables:retrieve_instruction_memory(Thread2) of
{ok, MemoryValue} ->
MemoryValue;
{error, not_found} ->
erlang:monotonic_time(millisecond)
end,
- WaitFinished = StartTime + binary_to_integer(Seconds) * 1000 < erlang:monotonic_time(millisecond),
+ MsToWait = case Value of
+ B when is_binary(B) ->
+ binary_to_integer(B) * 1000;
+ N when is_number(N) ->
+ N * 1000
+ end,
+
+ WaitFinished = StartTime + MsToWait < erlang:monotonic_time(millisecond),
case WaitFinished of
true ->
- NextIteration = automate_bot_engine_variables:unset_instruction_memory(Thread),
- {ran_this_tick, increment_position(NextIteration)};
+ Thread3 = automate_bot_engine_variables:unset_instruction_memory(Thread2),
+ {ran_this_tick, increment_position(Thread3), [WaitFinished]};
false ->
- NextIteration = automate_bot_engine_variables:set_instruction_memory(Thread, StartTime),
- {did_not_run, {new_state, NextIteration}}
+ Thread3 = automate_bot_engine_variables:set_instruction_memory(Thread2, StartTime),
+ {did_not_run, {new_state, Thread3}}
end;
-run_instruction(#{ ?TYPE := ?COMMAND_ADD_TO_LIST
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := ListName
- }
- , NewValueArg
- ]
- }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
+run_instruction(Op=#{ ?TYPE := ?COMMAND_ADD_TO_LIST
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , NewValueArg
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
- {ok, NewValue} = automate_bot_engine_variables:resolve_argument(NewValueArg, Thread),
- ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+
+ {ok, NewValue, Thread2} = automate_bot_engine_variables:resolve_argument(NewValueArg, Thread, Op),
+ ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread2, ListName) of
{ok, Value} ->
Value;
{error, not_found} ->
@@ -255,97 +600,195 @@ run_instruction(#{ ?TYPE := ?COMMAND_ADD_TO_LIST
%% TODO (optimization) avoid using list++list
ValueAfter = ValueBefore ++ [NewValue],
- {ok, NewThreadState } = automate_bot_engine_variables:set_program_variable(Thread, ListName, ValueAfter),
- {ran_this_tick, increment_position(NewThreadState)};
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, ListName, ValueAfter, ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread2), [ListName, NewValue]};
-run_instruction(#{ ?TYPE := ?COMMAND_DELETE_OF_LIST
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := ListName
- }
- , IndexValueArg
- ]
- }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
+run_instruction(Op=#{ ?TYPE := ?COMMAND_DELETE_OF_LIST
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , IndexValueArg
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
- {ok, IndexValue} = automate_bot_engine_variables:resolve_argument(IndexValueArg, Thread),
+ {ok, IndexValue, Thread2} = automate_bot_engine_variables:resolve_argument(IndexValueArg, Thread, Op),
Index = to_int(IndexValue),
- ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+ ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread2, ListName) of
{ok, Value} ->
Value;
{error, not_found} ->
- []
+ throw(#program_error{ error=#list_not_set{ list_name=ListName }
+ , block_id=?UTILS:get_block_id(Op)
+ })
end,
ValueAfter = automate_bot_engine_naive_lists:remove_nth(ValueBefore, Index),
- {ok, NewThreadState } = automate_bot_engine_variables:set_program_variable(Thread, ListName, ValueAfter),
- {ran_this_tick, increment_position(NewThreadState)};
-
-run_instruction(#{ ?TYPE := ?COMMAND_INSERT_AT_LIST
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := ListName
- }
- , ValueArg
- , IndexArg
- ]
- }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
-
- {ok, IndexValue} = automate_bot_engine_variables:resolve_argument(IndexArg, Thread),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, ListName, ValueAfter, ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread2), [ListName, Index]};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_DELETE_ALL_LIST
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, ListName, [], ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread), [ListName]};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_INSERT_AT_LIST
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , ValueArg
+ , IndexArg
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, IndexValue, Thread2} = automate_bot_engine_variables:resolve_argument(IndexArg, Thread, Op),
Index = to_int(IndexValue),
- {ok, Value} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread),
- ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+ {ok, Value, Thread3} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread2, Op),
+ ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread3, ListName) of
{ok, ListOnDB} ->
ListOnDB;
{error, not_found} ->
[]
end,
- ValueAfter = automate_bot_engine_naive_lists:insert_nth(ValueBefore, Index, Value),
-
- {ok, NewThreadState } = automate_bot_engine_variables:set_program_variable(Thread, ListName, ValueAfter),
- {ran_this_tick, increment_position(NewThreadState)};
-
-run_instruction(#{ ?TYPE := ?COMMAND_REPLACE_VALUE_AT_INDEX
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := ListName
- }
- , IndexArg
- , ValueArg
- ]
- }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
-
- {ok, IndexValue} = automate_bot_engine_variables:resolve_argument(IndexArg, Thread),
+ PaddedValue = automate_bot_engine_naive_lists:pad_to_length(
+ ValueBefore, IndexValue - 1, ?LIST_FILL), %% Remember: 1-indexed
+
+ ValueAfter = automate_bot_engine_naive_lists:insert_nth(PaddedValue, Index, Value),
+
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, ListName, ValueAfter, ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread3), [ListName, Index, Value]};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_SET_LIST
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , ValueArgument
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(ValueArgument, Thread, Op),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, ListName, Value, ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread2), [ListName, Value]};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_REPLACE_VALUE_AT_INDEX
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , IndexArg
+ , ValueArg
+ ]
+ }
+ , Thread=#program_thread{program_id=ProgramId}
+ , {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, IndexValue, Thread2} = automate_bot_engine_variables:resolve_argument(IndexArg, Thread, Op),
Index = to_int(IndexValue),
- {ok, Value} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread),
- ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+ {ok, Value, Thread3} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread2, Op),
+ ValueBefore = case automate_bot_engine_variables:get_program_variable(Thread3, ListName) of
{ok, ListOnDB} ->
ListOnDB;
{error, not_found} ->
[]
end,
- ValueAfter = automate_bot_engine_naive_lists:replace_nth(ValueBefore, Index, Value),
+ PaddedValue = automate_bot_engine_naive_lists:pad_to_length(
+ ValueBefore, IndexValue - 1, ?LIST_FILL), %% Remember: 1-indexed
+ ValueAfter = automate_bot_engine_naive_lists:replace_nth(PaddedValue, Index, Value),
- {ok, NewThreadState } = automate_bot_engine_variables:set_program_variable(Thread, ListName, ValueAfter),
- {ran_this_tick, increment_position(NewThreadState)};
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, ListName, ValueAfter, ?UTILS:get_block_id(Op)),
+ {ran_this_tick, increment_position(Thread3), [ListName, Index, Value]};
-run_instruction(#{ ?TYPE := ?COMMAND_CALL_SERVICE
- , ?ARGUMENTS := #{ ?SERVICE_ID := ServiceId
- , ?SERVICE_ACTION := Action
- , ?SERVICE_CALL_VALUES := Arguments
- }
- }, Thread=#program_thread{ program_id=ProgramId },
+run_instruction(Op=#{ ?TYPE := ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS := #{ ?SERVICE_ID := ServiceId
+ , ?SERVICE_ACTION := Action
+ , ?SERVICE_CALL_VALUES := Arguments
+ }
+ }, Thread=#program_thread{ program_id=ProgramId },
{?SIGNAL_PROGRAM_TICK, _}) ->
{ok, UserId} = automate_storage:get_program_owner(ProgramId),
- Values = lists:map(fun (Arg) ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(Arg, Thread),
- Value
- end, Arguments),
+ {Values, Thread2} = eval_args(Arguments, Thread, Op),
+
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ case automate_service_registry_query:call(Module, Action, Values, Thread2, UserId) of
+ {ok, Thread3, Value} ->
+ Thread4 = case ?UTILS:get_block_id(Op) of
+ none -> Thread3;
+ BlockId -> automate_bot_engine_variables:set_instruction_memory(Thread3, Value, BlockId)
+ end,
+ {ran_this_tick, increment_position(Thread4), [ServiceId, Action, Arguments]};
+ {error, Reason} ->
+ throw_bridge_call_error(Reason, ServiceId, Op, Action)
+ end;
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_SIGNAL_WAIT_FOR_PULSE
+ , ?ARGUMENTS := Arguments
+ }, Thread=#program_thread{ program_id=_ProgramId },
+ {?SIGNAL_PROGRAM_TICK, _}) ->
+ %% This doesn't do much, but is used to activate flows starting with FLOW_ON_BLOCK_RUN.
+ {[Value], Thread2} = eval_args(Arguments, Thread, Op),
+ Thread3 = case ?UTILS:get_block_id(Op) of
+ none -> Thread2;
+ BlockId -> automate_bot_engine_variables:set_instruction_memory(Thread2, Value, BlockId)
+ end,
+ {ran_this_tick, increment_position(Thread3), [Value]};
+
+run_instruction(#{ ?TYPE := ?COMMAND_BROADCAST_TO_ALL_USERS
+ }, Thread=#program_thread{ program_id=_ProgramId },
+ {?SIGNAL_PROGRAM_TICK, _}) ->
+
+
+ Thread2 = case automate_bot_engine_variables:retrieve_thread_value(Thread, ?UI_TRIGGER_VALUES) of
+ {ok, Val=#{ ?UI_TRIGGER_CONNECTION := _Source }} ->
+ {ok, T} = automate_bot_engine_variables:set_thread_value(Thread, ?UI_TRIGGER_VALUES, maps:remove(?UI_TRIGGER_CONNECTION, Val)),
+ T;
+ _ ->
+ Thread
+ end,
+ {ran_this_tick, increment_position(Thread2), []};
+
+
+run_instruction(Operation=#{ ?TYPE := <<"services.ui.", UiElement/binary>>
+ , ?ARGUMENTS := Arguments
+ }, Thread=#program_thread{ program_id=ProgramId },
+ {?SIGNAL_PROGRAM_TICK, _}) ->
+ {Values, Thread2} = eval_args(Arguments, Thread, Operation),
+
+ CommandData = #{ <<"key">> => ui_events_show
+ , <<"subkey">> => UiElement
+ , <<"values">> => Values
+ },
+
+ %% Trigger element update
+ case automate_bot_engine_variables:retrieve_thread_value(Thread, ?UI_TRIGGER_VALUES) of
+ {ok, #{ ?UI_TRIGGER_CONNECTION := Source }} ->
+ %% If we're in a specific user's flow
+ %% - Don't persist the widget value
+ %% - Send it directly to the user's session process
+ ok = automate_channel_engine:send_to_process(Source, CommandData);
+ _ ->
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+ ok = automate_storage:set_widget_value(ProgramId, UiElement, Values),
+ ok = automate_channel_engine:send_to_channel(ChannelId, CommandData)
+ end,
+
+ {ran_this_tick, increment_position(Thread2), [UiElement, Values]};
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, NewThread, _Value} = automate_service_registry_query:call(Module, Action, Values, Thread, UserId),
- {ran_this_tick, increment_position(NewThread)};
run_instruction(Operation=#{ ?TYPE := <<"services.", ServiceCall/binary>>
, ?ARGUMENTS := Arguments
@@ -356,81 +799,421 @@ run_instruction(Operation=#{ ?TYPE := <<"services.", ServiceCall/binary>>
{ok, UserId} = automate_storage:get_program_owner(ProgramId),
ReadArguments = remove_save_to(Arguments, SaveTo),
- Values = lists:map(fun (Arg) ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(Arg, Thread),
- Value
- end, ReadArguments),
+ {Values, Thread2} = eval_args(ReadArguments, Thread, Operation),
[ServiceId, Action] = binary:split(ServiceCall, <<".">>),
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, NewThread, Value} = automate_service_registry_query:call(Module, Action, Values, Thread, UserId),
-
- {ok, SavedThread} = case SaveTo of
- { index, Index } ->
- #{ <<"value">> := VariableName
- } = lists:nth(Index, Arguments),
- automate_bot_engine_variables:set_program_variable(
- Thread,
- %% Note that erlang is 1-indexed, protocol is 0-indexed
- VariableName,
- Value);
- _ ->
- {ok, NewThread}
- end,
- {ran_this_tick, increment_position(SavedThread)};
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ case automate_service_registry_query:call(Module, Action, Values, Thread2, UserId) of
+ {ok, Thread3, Value} ->
+ ok = case SaveTo of
+ { index, Index } ->
+ #{ <<"value">> := VariableName
+ } = lists:nth(Index, Arguments),
+ automate_bot_engine_variables:set_program_variable(
+ ProgramId,
+ %% Note that erlang is 1-indexed, protocol is 0-indexed
+ VariableName,
+ Value, ?UTILS:get_block_id(Operation));
+ _ ->
+ ok
+ end,
+ {ran_this_tick, increment_position(Thread3), [Values]};
+ {error, Reason} ->
+ throw_bridge_call_error(Reason, ServiceId, Operation, Action)
+ end;
-run_instruction(#{ ?TYPE := ?MATCH_TEMPLATE_STATEMENT
- , ?ARGUMENTS := [#{ ?TYPE := ?TEMPLATE_NAME_TYPE
- , ?VALUE := TemplateId
- }
- , Input
- ]
- }, Thread=#program_thread{ program_id=ProgramId },
+
+run_instruction(Op=#{ ?TYPE := ?MATCH_TEMPLATE_STATEMENT
+ , ?ARGUMENTS := [#{ ?TYPE := ?TEMPLATE_NAME_TYPE
+ , ?VALUE := TemplateId
+ }
+ , Input
+ ]
+ }, Thread=#program_thread{ program_id=ProgramId },
{?SIGNAL_PROGRAM_TICK, _}) ->
{ok, UserId} = automate_storage:get_program_owner(ProgramId),
- {ok, InputValue} = automate_bot_engine_variables:resolve_argument(Input, Thread),
+ {ok, InputValue, Thread2} = automate_bot_engine_variables:resolve_argument(Input, Thread, Op),
- case automate_template_engine:match(UserId, Thread, TemplateId, InputValue) of
+ case automate_template_engine:match(UserId, Thread2, TemplateId, InputValue) of
{ok, NewThread, _Value} ->
- {ran_this_tick, increment_position(NewThread)};
+ {ran_this_tick, increment_position(NewThread), [TemplateId, InputValue]};
{error, not_found} ->
- {ran_this_tick, finish_thread(Thread)}
+ {ran_this_tick, finish_thread(Thread2), [TemplateId, InputValue]}
end;
-run_instruction(#{ ?TYPE := ?COMMAND_CUSTOM_SIGNAL
- , ?ARGUMENTS := [ SignalIdVal
- , SignalDataVal
- ]
- }, Thread=#program_thread{ program_id=_ProgramId },
+run_instruction(Op=#{ ?TYPE := ?COMMAND_CUSTOM_SIGNAL
+ , ?ARGUMENTS := [ SignalIdVal
+ , SignalDataVal
+ ]
+ }, Thread=#program_thread{ program_id=_ProgramId },
{?SIGNAL_PROGRAM_TICK, _}) ->
- {ok, ChannelId } = automate_bot_engine_variables:resolve_argument(SignalIdVal, Thread),
- {ok, SignalData } = automate_bot_engine_variables:resolve_argument(SignalDataVal, Thread),
+ {ok, ChannelId, Thread2 } = automate_bot_engine_variables:resolve_argument(SignalIdVal, Thread, Op),
+ {ok, SignalData, Thread3 } = automate_bot_engine_variables:resolve_argument(SignalDataVal, Thread2, Op),
ok = automate_channel_engine:send_to_channel(ChannelId, SignalData),
- {ran_this_tick, increment_position(Thread)};
+ {ran_this_tick, increment_position(Thread3), [ChannelId, SignalData]};
+
+run_instruction(Op=#{ ?TYPE := ?CONTEXT_SELECT_CONNECTION
+ , ?ARGUMENTS := [ BridgeIdVal
+ , ConnectionIdVal
+ ]
+ }, Thread=#program_thread{ position=Position, direction=Direction },
+ {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, BridgeId, Thread2 } = automate_bot_engine_variables:resolve_argument(BridgeIdVal, Thread, Op),
+ {ok, ConnectionId, Thread3 } = automate_bot_engine_variables:resolve_argument(ConnectionIdVal, Thread2, Op),
+
+ case Direction of
+ up ->
+ %% Already here, exit the context
+ Thread4 = automate_bot_engine_variables:unset_instruction_memory(Thread3),
+ {ran_this_tick, increment_position(Thread4), [Direction, BridgeId, ConnectionId]};
+ forward ->
+ Thread4 = automate_bot_engine_variables:set_instruction_memory(
+ Thread3, [ { context_group
+ , bridge_connection
+ , {BridgeId, ConnectionId}
+ }]),
+ { ran_this_tick
+ , Thread4#program_thread{ position=Position ++ [1], direction=forward }
+ , [Direction, BridgeId, ConnectionId]
+ }
+ end;
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_PRELOAD_GETTER
+ , ?ARGUMENTS := [ Arg=#{ ?TYPE := ?VARIABLE_BLOCK
+ , ?VALUE := _Block
+ }
+ ]
+ }, Thread=#program_thread{ program_id=_ProgramId },
+ {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, Result, Thread2 } = automate_bot_engine_variables:resolve_argument(Arg, Thread, Op),
+ {ran_this_tick, increment_position(Thread2), [Result]};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_LOG_VALUE
+ , ?ARGUMENTS := [ Arg
+ ]
+ }, Thread=#program_thread{ program_id=ProgramId },
+ {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ {ok, ArgValue, Thread2 } = automate_bot_engine_variables:resolve_argument(Arg, Thread, Op),
+ Format = case ArgValue of
+ X when is_binary(X) ->
+ "~s";
+ _ ->
+ "~p"
+ end,
+ Message = binary:list_to_bin(
+ lists:flatten(io_lib:format(Format, [ArgValue]))),
+ ok = automate_logging:add_user_generated_program_log(#user_generated_log_entry{
+ severity=debug,
+ program_id=ProgramId,
+ block_id=?UTILS:get_block_id(Op),
+ event_message=Message,
+ event_time=os:system_time(millisecond)
+ }),
+
+ {ran_this_tick, increment_position(Thread2), [Message]};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_FORK_EXECUTION
+ , ?ARGUMENTS := Arguments
+ , ?CONTENTS := Flows
+ }, Thread=#program_thread{ program_id=ProgramId, position=Position, direction=Direction },
+ {?SIGNAL_PROGRAM_TICK, _}) ->
+
+ %% TODO: Consider actively signaling the parent when children end
+ case automate_bot_engine_variables:retrieve_instruction_memory(Thread) of
+ %% Parent thread start, fork children
+ {error, not_found} ->
+ Thread2 = automate_bot_engine_variables:set_instruction_memory(
+ Thread, #{ already_run => true }),
+
+ {ComputedArgs, Thread3} = eval_args(Arguments, Thread2, Op),
+ ContinuationType = case lists:search(fun(X) -> X =:= ?OP_FORK_CONTINUE_ON_FIRST end, ComputedArgs) of
+ { value, _ } ->
+ continue_on_first_done;
+ _ ->
+ continue_when_all_done
+ end,
+
+ ChildrenIds = lists:map(fun(Index) ->
+ {ok, NewThreadId } = automate_bot_engine_thread_launcher:launch_thread(
+ ProgramId,
+ Thread3#program_thread{position=Position ++ [Index, 1], direction=forward}),
+ NewThreadId
+ end, lists:seq(1, length(Flows))),
+
+ Thread4 = automate_bot_engine_variables:set_instruction_memory(
+ Thread3, #{ already_run => true
+ , children => ChildrenIds
+ , continuation_type => ContinuationType
+ }),
+ %% Note that position is not incremented, so this instruction keeps
+ %% executing until all the children end
+ {ran_this_tick, Thread4, []};
+ %% Parent keeps executing and periodically checks if children did finish
+ {ok, #{ children := Children, already_run := true, continuation_type := ContinuationType } } ->
+ {RemainingChildren, CompletedChildren} = lists:partition(
+ fun(ChildId) ->
+ {ok, Value} = automate_storage:dirty_is_thread_alive(ChildId),
+ Value
+ end, Children),
+ ForkDone = (((ContinuationType =:= continue_when_all_done) and (length(RemainingChildren) =:= 0))
+ or ((ContinuationType =:= continue_on_first_done) and (length(CompletedChildren) > 0))),
+ case ForkDone of
+ true ->
+ Thread2 = automate_bot_engine_variables:unset_instruction_memory(Thread),
+ {ran_this_tick, increment_position(Thread2), []};
+ false ->
+ Thread2 = automate_bot_engine_variables:set_instruction_memory(
+ Thread, #{ already_run => true
+ , children => RemainingChildren
+ , continuation_type => ContinuationType
+ }),
+ {did_not_run, {new_state, Thread2}}
+ end;
+ %% Children thread, just finish thread
+ {ok, #{ already_run := true }} ->
+ case Direction of
+ up ->
+ %% Normal execution
+ {stopped, thread_finished};
+ forward ->
+ %% Trying to fork after a JUMP back
+ %% Log an error and stop it.
+ %% TODO: Think for a reasonable scenario that would require supporting this.
+ automate_logging:log_platform(warning, io_lib:format("[~p:~p] FORK from child after JMP at on (programId=~p)",
+ [?MODULE, ?LINE, ProgramId])),
+
+ {stopped, thread_finished}
+ end
+ end;
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := VarName
+ }
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
+ CurrentValue = case automate_bot_engine_variables:get_program_variable(Thread, VarName) of
+ {ok, Val} ->
+ Val;
+ {error, not_found} ->
+ not_found
+ end,
+ case automate_bot_engine_variables:retrieve_instruction_memory(Thread) of
+ {error, not_found} ->
+ %% Initial run, save current value and wait
+ Thread2 = automate_bot_engine_variables:set_instruction_memory(Thread, #{ initial_value => CurrentValue }),
+ {did_not_run, {new_state, Thread2}};
+ {ok, #{ initial_value := CurrentValue } } ->
+ %% Non-initial run, variable value did NOT change (old matches with current)
+ {did_not_run, waiting};
+ {ok, #{ initial_value := _OtherValue }} ->
+ %% Non-initial run, variable DID change
+ Thread2 = case ?UTILS:get_block_id(Op) of
+ none ->
+ Thread;
+ Id ->
+ automate_bot_engine_variables:set_instruction_memory(Thread,
+ CurrentValue,
+ Id)
+ end,
+ {ran_this_tick, increment_position(Thread2), [VarName]}
+ end;
-run_instruction(#{ ?TYPE := Instruction }, _Thread, Message) ->
- io:format("Unhandled instruction/msg: ~p/~p~n", [Instruction, Message]),
+run_instruction(#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ _Block
+ ]
+ }, _Thread, {?SIGNAL_PROGRAM_TICK, _}) ->
+ %% This must not advace on tick, only when a new value (of the listened block) is passed
+ {did_not_run, waiting};
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_BLOCK
+ , ?VALUE := [ Listened=#{ ?TYPE := <<"services.", MonitorPath/binary>>
+ }
+ ]
+ }
+ ]
+ },
+ Thread=#program_thread{ program_id=_ProgramId },
+ { ?TRIGGERED_BY_MONITOR, {_MonitorId, Message=#{ <<"key">> := MessageKey, <<"service_id">> := BridgeId }} }) ->
+
+ [ServiceId, MonitorKey] = binary:split(MonitorPath, <<".">>),
+
+ case BridgeId of
+ ServiceId ->
+ SubKeyMatch = case Listened of
+ #{ ?ARGUMENTS := Arguments } ->
+ case {?UTILS:get_block_key_subkey(Arguments), Message} of
+ {{ key_and_subkey, _Key, SubKey }, #{ <<"subkey">> := SubKey }} ->
+ %% Subkey match
+ true;
+ {{key_and_subkey, _Key, _SubKey}, _} ->
+ %% Subkey NO match
+ false;
+ _ ->
+ true %% Subkey not present
+ end;
+ _ ->
+ %% No arguments, so key match is enough
+ true
+ end,
+ KeyMatch = (MonitorKey =:= MessageKey) and SubKeyMatch,
+ case KeyMatch of
+ true ->
+ %% Save content if appropriate
+ Thread2 = case Message of
+ #{ ?CHANNEL_MESSAGE_CONTENT := Content } ->
+ case ?UTILS:get_block_id(Op) of
+ none ->
+ Thread;
+ Id ->
+ automate_bot_engine_variables:set_instruction_memory(Thread,
+ Content,
+ Id)
+ end;
+ _ -> Thread
+ end,
+
+ {ran_this_tick, increment_position(Thread2), [ServiceId, MonitorKey]};
+ false ->
+ automate_logging:log_platform(warning,
+ io_lib:format("Unexpected signal (key did't match) ~p for block: ~p",
+ [Message, Listened])),
+ {did_not_run, waiting}
+ end;
+ _ ->
+ automate_logging:log_platform(warning,
+ io_lib:format("Unexpected signal (monitor didn't match) ~p for block: ~p",
+ [Message, Listened])),
+ {did_not_run, waiting}
+ end;
+
+run_instruction(Op=#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_BLOCK
+ , ?VALUE := [ #{ ?TYPE := ?WAIT_FOR_MONITOR
+ , ?ARGUMENTS := MonArgs=#{ ?FROM_SERVICE := ServiceId }
+ }
+ ]
+ }
+ ]
+ },
+ Thread,
+ { ?TRIGGERED_BY_MONITOR, {_MonitorId, Message=#{ <<"service_id">> := ServiceId }} }) ->
+
+ Accepted = case MonArgs of
+ #{ <<"key">> := ExpectedKey } ->
+ case Message of
+ #{ <<"key">> := ExpectedKey} ->
+ true;
+ _ ->
+ false
+ end;
+ _ -> %% No key required
+ true
+ end,
+ case Accepted of
+ false ->
+ {did_not_run, waiting};
+ true ->
+ Thread2 = case Message of
+ #{ ?CHANNEL_MESSAGE_CONTENT := Content } ->
+ case ?UTILS:get_block_id(Op) of
+ none ->
+ Thread;
+ Id ->
+ automate_bot_engine_variables:set_instruction_memory(Thread,
+ Content,
+ Id)
+ end;
+ _ -> Thread
+ end,
+
+ {ran_this_tick, increment_position(Thread2), []}
+ end;
+
+run_instruction(Op=#{ ?TYPE := ?WAIT_FOR_MONITOR
+ , ?ARGUMENTS := MonArgs=#{ ?MONITOR_ID := #{ ?FROM_SERVICE := ServiceId } }
+ },
+ Thread,
+ { ?TRIGGERED_BY_MONITOR, {_MonitorId, Message=#{ <<"service_id">> := ServiceId }} }) ->
+
+ Accepted = case MonArgs of
+ #{ <<"key">> := ExpectedKey } ->
+ case Message of
+ #{ <<"key">> := ExpectedKey} ->
+ true;
+ _ ->
+ false
+ end;
+ _ -> %% No key required
+ true
+ end,
+ case Accepted of
+ false ->
+ {did_not_run, waiting};
+ true ->
+ Thread2 = case ?UTILS:get_block_id(Op) of
+ none ->
+ Thread;
+ Id ->
+ automate_bot_engine_variables:set_instruction_memory(Thread,
+ Message,
+ Id)
+ end,
+
+ {ran_this_tick, increment_position(Thread2), [MonArgs]}
+ end;
+
+run_instruction(#{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := [ Block
+ ]
+ }, _Thread, Message) ->
+ automate_logging:log_platform(warning, io_lib:format("Got unexpected signal ~p for block: ~p",
+ [Message, Block])),
+
+ {did_not_run, waiting};
+
+run_instruction(#{ ?TYPE := ?FLOW_JUMP_TO_POSITION
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := [ PosHead | PosTail ]
+ }
+ ]
+ }, Thread, _Message) ->
+
+ %% The position head is not incremented, as the trigger is not present on
+ %% the "Thread" structure anyway, so this gap is naturally skipped.
+ ToInternalPosition = [ PosHead | lists:map(fun(SubPos) -> SubPos + 1 end, PosTail)],
+ {ran_this_tick, Thread#program_thread{position=ToInternalPosition, direction=forward}, [ToInternalPosition]};
+
+run_instruction(#{ ?TYPE := Instruction },
+ #program_thread{ program_id=ProgramId },
+ Message) ->
+ automate_logging:log_platform(
+ warning,
+ io_lib:format("Unhandled instruction/msg [programId=~p]: ~p/~p", [ProgramId, Instruction, Message])),
{did_not_run, waiting};
run_instruction(#{ <<"contents">> := _Content }, Thread, _Message) ->
%% Finished code block
- {ran_this_tick, increment_position(Thread)}.
+ {ran_this_tick, increment_position(Thread), []}.
increment_position(Thread = #program_thread{position=Position}) ->
IncrementedInnermost = increment_innermost(Position),
- BackToParent = back_to_parent(Position),
- FollowInSameLevelState = Thread#program_thread{position=IncrementedInnermost},
- BackToParentState = Thread#program_thread{position=BackToParent},
+ FollowInSameLevelState = Thread#program_thread{position=IncrementedInnermost, direction=forward},
case get_instruction(FollowInSameLevelState) of
{ok, _} ->
FollowInSameLevelState;
{error, element_not_found} ->
- BackToParentState
+ BackToParent = back_to_parent(Position),
+ Thread#program_thread{position=BackToParent, direction=up}
end.
@@ -441,7 +1224,7 @@ to_int(Value) when is_binary(Value) ->
IntValue.
finish_thread(Thread = #program_thread{}) ->
- Thread#program_thread{position=[]}.
+ Thread#program_thread{position=[]}. %% Direction is irrelevant
back_to_parent([]) ->
[1];
@@ -460,244 +1243,286 @@ increment_innermost(List)->
lists:reverse([Latest + 1 | Tail]).
%%%% Operators
-%% String operators
-get_block_result(#{ ?TYPE := ?COMMAND_JOIN
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
-
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:join(FirstValue, SecondValue);
- _ ->
- {error, not_found}
- end;
-get_block_result(#{ ?TYPE := ?COMMAND_JSON
- , ?ARGUMENTS := [ KeyReference
- , MapReference
- ]
- }, Thread) ->
- KeyResult = automate_bot_engine_variables:resolve_argument(KeyReference, Thread),
- MapResult = automate_bot_engine_variables:resolve_argument(MapReference, Thread),
+-spec run_getter_block(map(), #program_thread{}) -> {ok, any(), #program_thread{}} | {error, not_found}.
+run_getter_block(Op, Thread) ->
+ case get_block_result(Op, Thread) of
+ {error, Reason} ->
+ {error, Reason};
+ {ok, Result, Thread2} ->
+ Thread3 = case ?UTILS:get_block_id(Op) of
+ none ->
+ Thread2;
+ Id ->
+ automate_bot_engine_variables:set_instruction_memory(Thread2,
+ Result,
+ Id)
+ end,
+ {ok, Result, Thread3}
+ end.
- case [KeyResult, MapResult] of
- [{ok, KeyValue}, {ok, MapValue}] ->
- automate_bot_engine_values:get_value_by_key(KeyValue, MapValue);
- _ ->
- {error, not_found}
+%% String operators
+-spec get_block_result(map(), #program_thread{}) -> {ok, any(), #program_thread{}} | {error, not_found}.
+get_block_result(Op=#{ ?TYPE := ?COMMAND_JOIN
+ , ?ARGUMENTS := OpArgs
+ }, Thread) ->
+
+ Default = <<"">>,
+ {Args, Thread2} = eval_args_with_default(OpArgs, Thread, Op, Default),
+ [FirstVal, SecondVal] = case Args of
+ [_, _] -> Args;
+ [Left] -> [Left, Default];
+ [] -> [Default, Default]
+ end,
+
+ %% TODO: Consider how this can be made variadic
+ {ok, Value} = automate_bot_engine_values:join(FirstVal, SecondVal),
+ {ok, Value, Thread2};
+
+get_block_result(Op=#{ ?TYPE := ?COMMAND_STRING_CONTAINS
+ , ?ARGUMENTS := [ Haystack
+ , Needle
+ ]
+ }, Thread) ->
+
+ {[HaystackVal, NeedleVal], Thread2} = eval_args([Haystack, Needle], Thread, Op),
+ {ok, Value} = automate_bot_engine_values:string_contains(HaystackVal, NeedleVal),
+ {ok, Value, Thread2};
+
+get_block_result(Op=#{ ?TYPE := ?COMMAND_JSON
+ , ?ARGUMENTS := [ KeyReference
+ , MapReference
+ ]
+ }, Thread) ->
+
+ {[Key, Map], Thread2} = eval_args([KeyReference, MapReference], Thread, Op),
+ case automate_bot_engine_values:get_value_by_key(Key, Map) of
+ {ok, Value} ->
+ {ok, Value, Thread2};
+ Error ->
+ Error
end;
-
-
%% Templates
-get_block_result(#{ ?TYPE := ?MATCH_TEMPLATE_CHECK
- , ?ARGUMENTS := [#{ ?TYPE := ?TEMPLATE_NAME_TYPE
- , ?VALUE := TemplateId
- }
- , Input
- ]
- }, Thread=#program_thread{ program_id=ProgramId }) ->
+get_block_result(Op=#{ ?TYPE := ?MATCH_TEMPLATE_CHECK
+ , ?ARGUMENTS := [#{ ?TYPE := ?TEMPLATE_NAME_TYPE
+ , ?VALUE := TemplateId
+ }
+ , Input
+ ]
+ }, Thread=#program_thread{ program_id=ProgramId }) ->
{ok, UserId} = automate_storage:get_program_owner(ProgramId),
- {ok, InputValue} = automate_bot_engine_variables:resolve_argument(Input, Thread),
-
- case automate_template_engine:match(UserId, Thread, TemplateId, InputValue) of
- {ok, NewThread, _Value} ->
- {ok, true};
+ {ok, InputValue, Thread2} = automate_bot_engine_variables:resolve_argument(Input, Thread, Op),
+ case automate_template_engine:match(UserId, Thread2, TemplateId, InputValue) of
+ {ok, Thread3, _Value} ->
+ {ok, true, Thread3};
{error, not_found} ->
{ok, false}
end;
%% Numeric operators
-get_block_result(#{ ?TYPE := ?COMMAND_ADD
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:add(FirstValue, SecondValue);
- _ ->
- {error, not_found}
- end;
-
-get_block_result(#{ ?TYPE := ?COMMAND_SUBTRACT
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:subtract(FirstValue, SecondValue);
- _ ->
- {error, not_found}
+get_block_result(Op=#{ ?TYPE := ?COMMAND_ADD
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ %% TODO: Consider how this can be made variadic
+ case automate_bot_engine_values:add(FirstValue, SecondValue) of
+ {ok, Value} ->
+ {ok, Value, Thread2}
end;
-get_block_result(#{ ?TYPE := ?COMMAND_MULTIPLY
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:multiply(FirstValue, SecondValue);
- _ ->
- {error, not_found}
+get_block_result(Op=#{ ?TYPE := ?COMMAND_SUBTRACT
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ %% TODO: Consider how this can be made variadic
+ case automate_bot_engine_values:subtract(FirstValue, SecondValue) of
+ {ok, Value} ->
+ {ok, Value, Thread2};
+ Error ->
+ Error
end;
-get_block_result(#{ ?TYPE := ?COMMAND_DIVIDE
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:divide(FirstValue, SecondValue);
- _ ->
- {error, not_found}
+get_block_result(Op=#{ ?TYPE := ?COMMAND_MULTIPLY
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ %% TODO: Consider how this can be made variadic
+ case automate_bot_engine_values:multiply(FirstValue, SecondValue) of
+ {ok, Value} ->
+ {ok, Value, Thread2};
+ Error ->
+ Error
end;
-%% Comparations
-get_block_result(#{ ?TYPE := ?COMMAND_LESS_THAN
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:is_less_than(FirstValue, SecondValue);
- _ ->
- {error, not_found}
+get_block_result(Op=#{ ?TYPE := ?COMMAND_DIVIDE
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ %% TODO: Consider how this can be made variadic
+ case automate_bot_engine_values:divide(FirstValue, SecondValue) of
+ {ok, Value} ->
+ {ok, Value, Thread2};
+ Error ->
+ Error
end;
-get_block_result(#{ ?TYPE := ?COMMAND_GREATER_THAN
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:is_greater_than(FirstValue, SecondValue);
- _ ->
- {error, not_found}
+get_block_result(Op=#{ ?TYPE := ?COMMAND_MODULO
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ %% TODO: Consider how this can be made variadic
+ case automate_bot_engine_values:modulo(FirstValue, SecondValue) of
+ {ok, Value} ->
+ {ok, Value, Thread2};
+ Error ->
+ Error
end;
-get_block_result(#{ ?TYPE := ?COMMAND_EQUALS
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, FirstValue}, {ok, SecondValue}] ->
- automate_bot_engine_values:is_equal_to(FirstValue, SecondValue);
- _ ->
- {error, not_found}
- end;
+%% Comparations
+get_block_result(Op=#{ ?TYPE := ?COMMAND_LESS_THAN
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ {ok, Value} = automate_bot_engine_values:is_less_than(FirstValue, SecondValue),
+ {ok, Value, Thread2};
+
+get_block_result(Op=#{ ?TYPE := ?COMMAND_GREATER_THAN
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ {ok, Value} = automate_bot_engine_values:is_greater_than(FirstValue, SecondValue),
+ {ok, Value, Thread2};
+
+get_block_result(Op=#{ ?TYPE := ?COMMAND_EQUALS
+ , ?ARGUMENTS := Args
+ }, Thread) ->
+ {Values, Thread2} = eval_args(Args, Thread, Op),
+ {ok, Result} = automate_bot_engine_values:are_equal(Values),
+ {ok, Result, Thread2};
%% Boolean operations
-get_block_result(#{ ?TYPE := ?COMMAND_AND
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, true}, {ok, true}] ->
- {ok, true};
- [_, _] ->
- {ok, false};
- _ ->
- {error, not_found}
+get_block_result(Op=#{ ?TYPE := ?COMMAND_AND
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ %% TODO: Consider how this can be made variadic
+ case {FirstValue, SecondValue} of
+ {true, true} ->
+ {ok, true, Thread2};
+ {error, Reason} ->
+ {error, Reason};
+ {_, _} ->
+ {ok, false, Thread2}
end;
-get_block_result(#{ ?TYPE := ?COMMAND_OR
- , ?ARGUMENTS := [ First
- , Second
- ]
- }, Thread) ->
- FirstResult = automate_bot_engine_variables:resolve_argument(First, Thread),
- SecondResult = automate_bot_engine_variables:resolve_argument(Second, Thread),
- case [FirstResult, SecondResult] of
- [{ok, true}, _] ->
- {ok, true};
- [_, {ok, true}] ->
- {ok, true};
- [_, _] ->
- {ok, false};
- _ ->
- {error, not_found}
+get_block_result(Op=#{ ?TYPE := ?COMMAND_OR
+ , ?ARGUMENTS := [ First
+ , Second
+ ]
+ }, Thread) ->
+ {[FirstValue, SecondValue], Thread2} = eval_args([First, Second], Thread, Op),
+ %% TODO: Consider how this can be made variadic
+ case {FirstValue, SecondValue} of
+ {true, _} ->
+ {ok, true, Thread2};
+ {_, true} ->
+ {ok, true, Thread2};
+ {error, Reason} ->
+ {error, Reason};
+ {_, _} ->
+ {ok, false, Thread2}
end;
-get_block_result(#{ ?TYPE := ?COMMAND_NOT
- , ?ARGUMENTS := [ Value
- ]
- }, Thread) ->
- Result = automate_bot_engine_variables:resolve_argument(Value, Thread),
+get_block_result(Op=#{ ?TYPE := ?COMMAND_NOT
+ , ?ARGUMENTS := [ Value
+ ]
+ }, Thread) ->
+ {ok, Result, Thread2} = automate_bot_engine_variables:resolve_argument(Value, Thread, Op),
case Result of
- {ok, false} ->
- {ok, true};
- {ok, true} ->
- {ok, false};
+ false ->
+ {ok, true, Thread2};
+ true ->
+ {ok, false, Thread2};
_ ->
{error, not_found}
end;
%% Variables
-get_block_result(#{ ?TYPE := ?COMMAND_DATA_VARIABLE
- , ?ARGUMENTS := [ Value
- ]
- }, Thread) ->
- automate_bot_engine_variables:resolve_argument(Value, Thread);
+get_block_result(Op=#{ ?TYPE := ?COMMAND_DATA_VARIABLE
+ , ?ARGUMENTS := [ Value
+ ]
+ }, Thread) ->
+ automate_bot_engine_variables:resolve_argument(Value, Thread, Op);
%% List
-get_block_result(#{ ?TYPE := ?COMMAND_ITEM_OF_LIST
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := ListName
- }
- , IndexArg
- ]
- }, Thread) ->
- {ok, IndexValue} = automate_bot_engine_variables:resolve_argument(IndexArg, Thread),
+get_block_result(Op=#{ ?TYPE := ?COMMAND_ITEM_OF_LIST
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , IndexArg
+ ]
+ }, Thread) ->
+ {ok, IndexValue, Thread2} = automate_bot_engine_variables:resolve_argument(IndexArg, Thread, Op),
Index = to_int(IndexValue),
- case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+ case automate_bot_engine_variables:get_program_variable(Thread2, ListName) of
{ok, List} ->
- automate_bot_engine_naive_lists:get_nth(List, Index);
+ case automate_bot_engine_naive_lists:get_nth(List, Index) of
+ {ok, Value } ->
+ {ok, Value, Thread2};
+ {error, not_found} ->
+ throw(#program_error{ error=#index_not_in_list{list_name=ListName, index=Index, max=length(List)}
+ , block_id=?UTILS:get_block_id(Op)
+ });
+ {error, invalid_list_index_type} ->
+ throw(#program_error{ error=#invalid_list_index_type{list_name=ListName, index=Index}
+ , block_id=?UTILS:get_block_id(Op)
+ })
+ end;
{error, not_found} ->
- {error, not_found}
+ throw(#program_error{ error=#list_not_set{ list_name=ListName }
+ , block_id=?UTILS:get_block_id(Op)
+ })
end;
-get_block_result(#{ ?TYPE := ?COMMAND_ITEMNUM_OF_LIST
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := ListName
- }
- , ValueArg
- ]
- }, Thread) ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread),
- case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+get_block_result(Op=#{ ?TYPE := ?COMMAND_ITEMNUM_OF_LIST
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , ValueArg
+ ]
+ }, Thread) ->
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread, Op),
+ case automate_bot_engine_variables:get_program_variable(Thread2, ListName) of
{ok, List} ->
- automate_bot_engine_naive_lists:get_item_num(List, Value);
+ case automate_bot_engine_naive_lists:get_item_num(List, Value) of
+ {ok, Index} ->
+ {ok, Index, Thread2};
+ {error, not_found} ->
+ {error, not_found}
+ end;
{error, not_found} ->
- {error, not_found}
+ throw(#program_error{ error=#list_not_set{ list_name=ListName }
+ , block_id=?UTILS:get_block_id(Op)
+ })
end;
get_block_result(#{ ?TYPE := ?COMMAND_LENGTH_OF_LIST
@@ -708,68 +1533,149 @@ get_block_result(#{ ?TYPE := ?COMMAND_LENGTH_OF_LIST
}, Thread) ->
case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
{ok, List} ->
- automate_bot_engine_naive_lists:get_length(List);
+ {ok, Value} = automate_bot_engine_naive_lists:get_length(List),
+ {ok, Value, Thread};
{error, not_found} ->
- []
+ {ok, 0, Thread}
end;
-get_block_result(#{ ?TYPE := ?COMMAND_LIST_CONTAINS_ITEM
- , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := ListName
- }
- , ValueArg
- ]
- }, Thread) ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread),
- case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+get_block_result(Op=#{ ?TYPE := ?COMMAND_LIST_CONTAINS_ITEM
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ , ValueArg
+ ]
+ }, Thread) ->
+ {ok, Value, Thread2} = automate_bot_engine_variables:resolve_argument(ValueArg, Thread, Op),
+ case automate_bot_engine_variables:get_program_variable(Thread2, ListName) of
{ok, List} ->
- {ok, automate_bot_engine_naive_lists:contains(List, Value)};
+ Found = automate_bot_engine_naive_lists:contains(List, Value),
+ {ok, Found, Thread2};
{error, not_found} ->
- {ok, false}
+ {ok, false, Thread2}
end;
get_block_result(#{ ?TYPE := <<"monitor.retrieve.", MonitorId/binary>>
, ?ARGUMENTS := []
- }, _Thread) ->
+ }, Thread) ->
case automate_monitor_engine:get_last_monitor_result(MonitorId) of
{ok, Result} ->
- {ok, Result};
- {error, not_found} ->
- {ok, false}
+ {ok, Result, Thread}
end;
-get_block_result(#{ ?TYPE := <<"services.", ServiceCall/binary>>
- , ?ARGUMENTS := Arguments
- }, Thread=#program_thread{ program_id=ProgramId }) ->
+get_block_result(Op=#{ ?TYPE := <<"services.", ServiceCall/binary>>
+ , ?ARGUMENTS := Arguments
+ }, Thread=#program_thread{ program_id=ProgramId }) ->
{ok, UserId} = automate_storage:get_program_owner(ProgramId),
- Values = lists:map(fun (Arg) ->
- {ok, Value} = automate_bot_engine_variables:resolve_argument(Arg, Thread),
- Value
- end, Arguments),
+ {Values, Thread2} = eval_args(Arguments, Thread, Op),
[ServiceId, Action] = binary:split(ServiceCall, <<".">>),
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, _NewThread, Value} = automate_service_registry_query:call(Module, Action, Values, Thread, UserId),
- {ok, Value};
-
-get_block_result(#{ ?TYPE := ?COMMAND_CALL_SERVICE
- , ?ARGUMENTS := #{ ?SERVICE_ID := ServiceId
- , ?SERVICE_ACTION := Action
- , ?SERVICE_CALL_VALUES := Values
- }
- }, Thread=#program_thread{ program_id=PID }) ->
-
- {ok, #user_program_entry{ user_id=UserId }} = automate_storage:get_program_from_id(PID),
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, _NewThread, Value} = automate_service_registry_query:call(Module, Action, Values, Thread, UserId),
- {ok, Value};
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ case automate_service_registry_query:call(Module, Action, Values, Thread2, UserId) of
+ {ok, Thread3, Value} ->
+ {ok, Value, Thread3};
+ {error, Reason} ->
+ throw_bridge_call_error(Reason, ServiceId, Op, Action)
+ end;
+
+
+get_block_result(Op=#{ ?TYPE := <<"services.", _ServiceCall/binary>> }, Thread) ->
+ get_block_result(Op#{ ?ARGUMENTS => [] }, Thread);
+
+get_block_result(Op=#{ ?TYPE := ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS := #{ ?SERVICE_ID := ServiceId
+ , ?SERVICE_ACTION := Action
+ , ?SERVICE_CALL_VALUES := Args
+ }
+ }, Thread=#program_thread{ program_id=PID }) ->
+
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(PID),
+ Arguments = case Args of
+ %% This first form was generated on the Scratch's
+ %% serialization, but it's not found on the getters and it's
+ %% redundant.
+ #{ ?ARGUMENTS := A } -> A;
+ _ -> Args
+ end,
+
+ {Values, Thread2} = eval_args(Arguments, Thread, Op),
+
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ case automate_service_registry_query:call(Module, Action, Values, Thread2, Owner) of
+ {ok, Thread3, Value} ->
+ {ok, Value, Thread3};
+ {error, Reason} ->
+ throw_bridge_call_error(Reason, ServiceId, Op, Action)
+ end;
+
+
+get_block_result(Op=#{ ?TYPE := ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := ListName
+ }
+ ]
+ }, Thread) ->
+ case automate_bot_engine_variables:get_program_variable(Thread, ListName) of
+ {ok, List} ->
+ {ok, List, Thread};
+ {error, not_found} ->
+ throw(#program_error{ error=#list_not_set{ list_name=ListName }
+ , block_id=?UTILS:get_block_id(Op)
+ })
+ end;
+
+get_block_result(Op=#{ ?TYPE := ?FLOW_LAST_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := BlockId
+ }
+ , #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := Index
+ }
+ ]
+ }, Thread) ->
+ case automate_bot_engine_variables:retrieve_instruction_memory(Thread, BlockId) of
+ {ok, Value} ->
+ Result = case Value of
+ #{ <<"as_list">> := AsArray} ->
+ lists:nth(Index + 1, AsArray);
+ _ ->
+ Value
+ end,
+ {ok, Result, Thread};
+ {error, not_found} ->
+ throw(#program_error{ error=#memory_not_set{ block_id=BlockId }
+ , block_id=?UTILS:get_block_id(Op)
+ })
+ end;
+
+get_block_result(#{ ?TYPE := ?COMMAND_GET_THREAD_ID
+ }, Thread=#program_thread{ thread_id=ThreadId }) ->
+ {ok, ThreadId, Thread};
+
+get_block_result(Op=#{ ?TYPE := ?COMMAND_UI_BLOCK_VALUE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := UiElement
+ }
+ ]
+ }, Thread=#program_thread{}) ->
+ case automate_bot_engine_variables:retrieve_thread_value(Thread, ?UI_TRIGGER_VALUES) of
+ {ok, #{ ?UI_TRIGGER_DATA := #{ UiElement := Value } }} ->
+ {ok, Value, Thread};
+ _ ->
+ throw(#program_error{ error=#memory_not_set{ block_id=UiElement }
+ , block_id=?UTILS:get_block_id(Op)
+ })
+ end;
%% Fail
get_block_result(Block, _Thread) ->
- io:format("Result from: ~p~n", [Block]),
- erlang:error(bad_operation).
+ automate_logging:log_platform(error, io_lib:format("Don't know how to get result from: ~p~n",
+ [Block])),
+ throw(#program_error{ error=#unknown_operation{}
+ , block_id=?UTILS:get_block_id(Block)
+ }).
get_save_to(#{ <<"save_to">> := #{ <<"type">> := <<"argument">>
@@ -783,3 +1689,76 @@ remove_save_to(Arguments, none) ->
Arguments;
remove_save_to(Arguments, {index, Index}) ->
automate_bot_engine_naive_lists:remove_nth(Arguments, Index).
+
+-spec eval_args([any()], #program_thread{}, map()) -> {[any()], #program_thread{}}.
+eval_args(Arguments, Thread, Op) ->
+ eval_args_handling_null(Arguments, Thread, Op,
+ fun() ->
+ automate_logging:log_platform(error, io_lib:format("[~p:~p] Null argument found on: ~p",
+ [?MODULE, ?LINE, Op])),
+ throw(#program_error{ error=#unknown_operation{}
+ , block_id=?UTILS:get_block_id(Op)
+ })
+ end).
+
+-spec eval_args_with_default([any()], #program_thread{}, map(), any()) -> {[any()], #program_thread{}}.
+eval_args_with_default(Arguments, Thread, Op, Default) ->
+ eval_args_handling_null(Arguments, Thread, Op,
+ fun() ->
+ Default
+ end).
+
+-spec eval_args_handling_null([any()], #program_thread{}, map(), function()) -> {[any()], #program_thread{}}.
+eval_args_handling_null(Arguments, Thread, Op, OnNull) ->
+ { Thread2, RevValues } = lists:foldl(
+ fun(Arg, {UpdThread, Values}) ->
+ case Arg of
+ null ->
+ {UpdThread, [OnNull() | Values] };
+ _ -> case automate_bot_engine_variables:resolve_argument(Arg, UpdThread, Op) of
+ {ok, Value, UpdThread2} ->
+ {UpdThread2, [ Value | Values ]};
+ {error, not_found} ->
+ automate_logging:log_platform(error, io_lib:format("[~p:~p] Cannot resolve argument: ~p",
+ [?MODULE, ?LINE, Arg])),
+ throw(#program_error{ error=#unknown_operation{}
+ , block_id=?UTILS:get_block_id(Op)
+ })
+ end
+ end
+ end,
+ { Thread, [ ] }, Arguments),
+ %% This lists:reverse could be avoided if we used `lists:foldr` instead of `lists:foldl`.
+ %% But according to erlang documentation [ http://erlang.org/doc/man/lists.html#foldr-3 ]
+ %% > foldl/3 is tail recursive and is usually preferred to foldr/3.
+ %%
+ %% Still, argument lists are going to be so small that the difference does not matter.
+ %%
+ %% With this in mind, it might be preferrable to read the arguments from
+ %% left to right, just to make thinking more intuitive whenever a bug
+ %% regarding argument parsing arises. (This is considering the reader's
+ %% native language is left-to-right, which might not be true...)
+ {lists:reverse(RevValues), Thread2}.
+
+
+%% Error construction
+throw_bridge_call_error(no_connection, ServiceId, Op, Action) ->
+ throw(#program_error{ error=#disconnected_bridge{bridge_id=ServiceId, action=Action}
+ , block_id=?UTILS:get_block_id(Op)
+ });
+throw_bridge_call_error(no_valid_connection, ServiceId, Op, Action) ->
+ throw(#program_error{ error=#bridge_call_connection_not_found{bridge_id=ServiceId, action=Action}
+ , block_id=?UTILS:get_block_id(Op)
+ });
+throw_bridge_call_error(timeout, ServiceId, Op, Action) ->
+ throw(#program_error{ error=#bridge_call_timeout{bridge_id=ServiceId, action=Action}
+ , block_id=?UTILS:get_block_id(Op)
+ });
+throw_bridge_call_error({failed, Reason}, ServiceId, Op, Action) ->
+ throw(#program_error{ error=#bridge_call_failed{reason=Reason, bridge_id=ServiceId, action=Action}
+ , block_id=?UTILS:get_block_id(Op)
+ });
+throw_bridge_call_error({error_getting_resource, _ST}, ServiceId, Op, Action) ->
+ throw(#program_error{ error=#bridge_call_error_getting_resource{bridge_id=ServiceId, action=Action}
+ , block_id=?UTILS:get_block_id(Op)
+ }).
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_program_decoder.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_program_decoder.erl
index d07dfc8a..4a3d5630 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_program_decoder.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_program_decoder.erl
@@ -4,10 +4,12 @@
%% Exposed functions
-export([ initialize_program/2
, update_program/2
+ , get_bridges_on_program/1
]).
-include("../../automate_storage/src/records.hrl").
-include("program_records.hrl").
+-include("instructions.hrl").
%%%===================================================================
%%% API
@@ -15,7 +17,7 @@
-spec initialize_program(binary(), #user_program_entry{}) -> {ok, #program_state{}}.
initialize_program(ProgramId,
#user_program_entry
- { user_id=OwnerUserId
+ { owner=OwnerUserId
, program_parsed=Parsed
, enabled=Enabled}) ->
@@ -30,7 +32,17 @@ initialize_program(ProgramId,
, permissions=#program_permissions{ owner_user_id=OwnerUserId }
, triggers=get_triggers(Blocks)
, enabled=Enabled
- }}
+ }};
+ X ->
+ automate_logging:log_platform(error, io_lib:format(
+ "When decoding ~p returned (unexpected): ~p~n",
+ [ ProgramId, X ])),
+ {ok, #program_state{ program_id=ProgramId
+ , variables=[]
+ , permissions=#program_permissions{ owner_user_id=OwnerUserId }
+ , triggers=[]
+ , enabled=Enabled
+ }}
catch ErrNS:Err:StackTrace ->
io:fwrite("\033[41;37m Error decoding program: ~p \033[0m~n", [{ErrNS, Err, StackTrace}]),
@@ -47,7 +59,7 @@ initialize_program(ProgramId,
-spec update_program(#program_state{}, #user_program_entry{}) -> {ok, #program_state{}}.
update_program(State,
#user_program_entry
- { user_id=OwnerUserId
+ { owner=OwnerUserId
, program_parsed=Parsed
, enabled=Enabled}) ->
{ok, #{ <<"variables">> := Variables
@@ -59,6 +71,12 @@ update_program(State,
, enabled=Enabled
}}.
+-spec get_bridges_on_program(#user_program_entry{}) -> { ok, [binary()] } | {error, not_found}.
+get_bridges_on_program(#user_program_entry{ program_parsed=undefined}) ->
+ {ok, []};
+get_bridges_on_program(#user_program_entry{ owner=OwnerUserId, program_parsed=Parsed}) ->
+ {ok, #{ <<"blocks">> := Columns } } = automate_program_linker:link_program(Parsed, OwnerUserId),
+ {ok, get_bridges_on_columns(Columns, OwnerUserId)}.
%%%===================================================================
%%% Internal functions
@@ -72,3 +90,75 @@ get_trigger([Trigger | Program]) ->
#program_trigger{ condition=Trigger
, subprogram=Program
}.
+
+get_bridges_on_columns(Columns, OwnerUserId) ->
+ Set = sets:from_list(lists:flatmap(fun(Column) ->
+ get_bridges_on_column(Column, OwnerUserId)
+ end, Columns)),
+ sets:to_list(Set).
+
+get_bridges_on_column(Column, OwnerUserId) ->
+ lists:flatmap(fun(Block) ->
+ get_bridges_on_block(Block, OwnerUserId)
+ end, Column).
+
+get_bridges_on_block(Block, OwnerUserId) ->
+ SubBlockBridges = get_subblock_bridges(Block, OwnerUserId),
+ ArgBridges = get_argument_bridges(Block, OwnerUserId),
+ ValueBridges = get_value_bridges(Block, OwnerUserId),
+ case get_bridge_on_block_call(Block, OwnerUserId) of
+ {ok, BridgeId} ->
+ [BridgeId | SubBlockBridges] ++ ArgBridges ++ ValueBridges;
+ {error, not_found} ->
+ SubBlockBridges ++ ArgBridges ++ ValueBridges
+ end.
+
+
+get_argument_bridges(#{ ?ARGUMENTS := Arguments } , OwnerUserId) when is_list(Arguments) ->
+ lists:flatmap(fun(Arg) -> get_bridges_on_block(Arg, OwnerUserId) end, Arguments);
+get_argument_bridges(#{ ?ARGUMENTS := Arguments } , OwnerUserId) when is_map(Arguments) ->
+ lists:flatmap(fun({_K, Arg}) -> get_bridges_on_block(Arg, OwnerUserId) end, maps:to_list(Arguments));
+get_argument_bridges(_Block, _OwnerUserId) ->
+ [].
+
+get_value_bridges(#{ ?VALUE := Values } , OwnerUserId) when is_list(Values) ->
+ lists:flatmap(fun(Val) -> get_bridges_on_block(Val, OwnerUserId) end, Values);
+get_value_bridges(#{ ?VALUE := Values } , OwnerUserId) when is_map(Values) ->
+ lists:flatmap(fun({_K, Val}) -> get_bridges_on_block(Val, OwnerUserId) end, maps:to_list(Values));
+get_value_bridges(_Block, _OwnerUserId) ->
+ [].
+
+
+get_subblock_bridges(#{<<"contents">> := Contents}, OwnerUserId) ->
+ lists:flatmap(fun (SubBlock) ->
+ get_bridges_on_block(SubBlock, OwnerUserId)
+ end, Contents);
+get_subblock_bridges(_, _) ->
+ [].
+
+
+get_bridge_on_block_call(#{ ?TYPE := ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS := #{ ?SERVICE_ID := ServiceId
+ }}, OwnerUserId) ->
+ service_id_to_bridge_id(ServiceId, OwnerUserId);
+
+get_bridge_on_block_call(#{ ?TYPE := <<"services.", ServiceCall/binary>>
+ }, OwnerUserId) ->
+ [ServiceId, _Action] = binary:split(ServiceCall, <<".">>),
+ service_id_to_bridge_id(ServiceId, OwnerUserId);
+
+get_bridge_on_block_call(_, _) ->
+ {error, not_found}.
+
+service_id_to_bridge_id(ServiceId, _OwnerUserId) ->
+ case automate_service_registry:get_service_by_id(ServiceId) of
+ {ok, #{ module := Module }} ->
+ case Module of
+ {automate_service_port_engine_service, [BridgeId]} ->
+ {ok, BridgeId};
+ _ ->
+ {error, not_found}
+ end;
+ {error, not_found} ->
+ {error, not_found}
+ end.
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_runner.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_runner.erl
index f4c9763c..2e5ff982 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_runner.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_runner.erl
@@ -135,10 +135,10 @@ loop(State = #state{ check_next_action=CheckContinue
State
end;
{false, _} ->
- io:format("\033[47;30mIgnoring (app stopped)\033[0m~n", []),
+ io:format("\033[47;30mIgnoring '~p' (app stopped)\033[0m~n", [ Signal ]),
State;
X ->
- io:format("\033[47;30mIgnoring ~p (not applicable)\033[0m~n", [X]),
+ io:format("[~p:~p]\033[47;30mIgnoring ~p (not applicable)\033[0m~n", [?MODULE, ?LINE, X]),
State
end,
loop(NextState);
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_runner.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_runner.erl
index ab06f76c..36880f78 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_runner.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_runner.erl
@@ -102,7 +102,7 @@ loop(State = #state{ check_next_action = CheckContinue
continue ->
run_tick(State, {Signal, Message});
X ->
- io:format("\033[47;30mIgnoring ~p (not applicable)\033[0m~n", [X]),
+ io:format("[~p:~p]\033[47;30mIgnoring ~p (not applicable)\033[0m~n", [?MODULE, ?LINE, X]),
State
end,
loop(NextState);
@@ -117,8 +117,9 @@ loop(State = #state{ check_next_action = CheckContinue
-spec run_tick(#state{}, any()) -> #state{}.
run_tick(State = #state{ thread=Thread }, Message) ->
+ #running_program_thread_entry{ thread_id=ThreadId } = Thread,
RunnerState = ?UTILS:parse_program_thread(Thread),
- {UpdateThread, NewRunnerState} = case automate_bot_engine_operations:run_thread(RunnerState, Message) of
+ {UpdateThread, NewRunnerState} = case automate_bot_engine_operations:run_thread(RunnerState, Message, ThreadId) of
{ stopped, _Reason } ->
self() ! {stop, self()},
{false, RunnerState}; %% Self-destroy
@@ -126,7 +127,7 @@ run_tick(State = #state{ thread=Thread }, Message) ->
{true, NewState};
{ did_not_run, _Reason } ->
{false, RunnerState};
- { ran_this_tick, RanThreadState } ->
+ { ran_this_tick, RanThreadState, _ } ->
#running_program_thread_entry{ parent_program_id=Ppid } = Thread,
automate_stats:log_observation(counter, automate_bot_engine_cycles, [Ppid]),
{true, RanThreadState}
@@ -142,7 +143,7 @@ run_tick(State = #state{ thread=Thread }, Message) ->
%% Trigger now the timer signal if needed
case lists:member(?SIGNAL_PROGRAM_TICK, ExpectedSignals) of
true ->
- timer:send_after(?MILLIS_PER_TICK, self(), {?SIGNAL_PROGRAM_TICK, {}});
+ erlang:send_after(?MILLIS_PER_TICK, self(), {?SIGNAL_PROGRAM_TICK, {}});
_ ->
ok
end,
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_utils.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_utils.erl
index 9fe4acf1..0cf3a3a8 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_utils.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_thread_utils.erl
@@ -14,26 +14,32 @@
%%%===================================================================
parse_program_thread(#running_program_thread_entry{ position=Position
+ , direction=Direction
, instructions=Instructions
, memory=Memory
, instruction_memory=InstructionMemory
, parent_program_id=ParentProgramId
+ , thread_id=ThreadId
}) ->
#program_thread{ position=Position
+ , direction=Direction
, program=Instructions
, global_memory=Memory
, instruction_memory=InstructionMemory
, program_id=ParentProgramId
+ , thread_id=ThreadId
}.
-spec merge_thread_structures(#running_program_thread_entry{}, #program_thread{}) -> #running_program_thread_entry{}.
merge_thread_structures(Thread, #program_thread{ position=Position
+ , direction=Direction
, program=Instructions
, global_memory=Memory
, instruction_memory=InstructionMemory
, program_id=ParentProgramId
}) ->
Thread#running_program_thread_entry{ position=Position
+ , direction=Direction
, instructions=Instructions
, memory=Memory
, instruction_memory=InstructionMemory
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_triggers.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_triggers.erl
index a0e32939..0b3e9618 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_triggers.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_triggers.erl
@@ -6,17 +6,21 @@
]).
-define(SERVER, ?MODULE).
+-define(UTILS, automate_bot_engine_utils).
-include("../../automate_storage/src/records.hrl").
-include("program_records.hrl").
-include("instructions.hrl").
-include("../../automate_channel_engine/src/records.hrl").
+-include("../../automate_common_types/src/protocol.hrl").
+-include("../../automate_services_time/src/definitions.hrl").
+-include("../../automate_testing/src/testing.hrl").
%%%===================================================================
%%% API
%%%===================================================================
-spec get_expected_signals(#program_state{}) -> {ok, [atom()]}.
-get_expected_signals(#program_state{triggers=Triggers, permissions=Permissions}) ->
- {ok, get_expected_signals_from_triggers(Triggers, Permissions)}.
+get_expected_signals(#program_state{program_id=ProgramId, triggers=Triggers, permissions=Permissions}) ->
+ {ok, get_expected_signals_from_triggers(Triggers, Permissions, ProgramId)}.
-spec get_triggered_threads(#program_state{}, {atom(), any()}) -> {ok, [#program_thread{}]}.
@@ -30,35 +34,121 @@ get_triggered_threads(Program=#program_state{triggers=Triggers}, Signal) ->
%%%===================================================================
%%%% Expected signals
--spec get_expected_signals_from_triggers([#program_trigger{}], #program_permissions{}) -> [atom()].
-get_expected_signals_from_triggers(Triggers, Permissions) ->
- [get_expected_action_from_trigger(Trigger, Permissions) || Trigger <- Triggers ].
-
--spec get_expected_action_from_trigger(#program_trigger{}, #program_permissions{}) -> atom().
+-spec get_expected_signals_from_triggers([#program_trigger{}], #program_permissions{}, binary()) -> [atom()].
+get_expected_signals_from_triggers(Triggers, Permissions, ProgramId) ->
+ lists:filtermap(fun(Trigger) ->
+ try get_expected_action_from_trigger(Trigger, Permissions, ProgramId) of
+ false ->
+ false;
+ Result ->
+ {true, Result}
+ catch ErrorNS:Error:StackTrace ->
+ automate_logging:log_platform(error, ErrorNS, Error, StackTrace),
+ false
+ end
+ end, Triggers).
+
+-spec get_expected_action_from_trigger(#program_trigger{}, #program_permissions{}, binary()) -> atom().
%% TODO: return a more specific monitor
+get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?WAIT_FOR_MONITOR
+ , ?ARGUMENTS := #{ ?MONITOR_ID := #{ ?FROM_SERVICE := ServiceId } }
+ }}, #program_permissions{owner_user_id=UserId}, _ProgramId) ->
+ ok = automate_service_registry_query:listen_service(ServiceId, UserId, { undefined, undefined }),
+ ?TRIGGERED_BY_MONITOR;
get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?WAIT_FOR_MONITOR
, ?ARGUMENTS := #{ ?MONITOR_ID := MonitorId }
- }}, _Permissions) ->
- automate_channel_engine:listen_channel(MonitorId),
+ }}, _Permissions, ProgramId) when is_binary(MonitorId) ->
+ ok = automate_channel_engine:listen_channel(MonitorId),
+ ?TRIGGERED_BY_MONITOR;
+
+
+get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := <<"services.ui.", UiMonitorPath/binary>>
+ }},
+ #program_permissions{owner_user_id=UserId}, ProgramId) ->
+
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+ automate_channel_engine:listen_channel(ChannelId, { ui_events, UiMonitorPath }),
+ ?TRIGGERED_BY_MONITOR;
+
+get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?COMMAND_DATA_VARIABLE_ON_CHANGE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := Variable
+ }
+ ]
+ }},
+ #program_permissions{owner_user_id=UserId}, ProgramId) ->
+
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+ automate_channel_engine:listen_channel(ChannelId, { variable_events, Variable }),
+ ?TRIGGERED_BY_MONITOR;
+
+get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?COMMAND_TRIGGER_ON_BRIDGE_CONNECTED
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := BridgeId
+ }
+ ]
+ }},
+ #program_permissions{owner_user_id=UserId}, _ProgramId) ->
+
+ ok = automate_service_registry_query:listen_service(BridgeId, UserId, { ?PROTO_ON_BRIDGE_CONNECTED, BridgeId }),
+ ?TRIGGERED_BY_MONITOR;
+
+get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?COMMAND_TRIGGER_ON_BRIDGE_DISCONNECTED
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := BridgeId
+ }
+ ]
+ }},
+ #program_permissions{owner_user_id=UserId}, _ProgramId) ->
+
+ ok = automate_service_registry_query:listen_service(BridgeId, UserId, { ?PROTO_ON_BRIDGE_DISCONNECTED, BridgeId }),
+ ?TRIGGERED_BY_MONITOR;
+
+get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?FLOW_ON_BLOCK_RUN
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := BlockId
+ }
+ , _ChangeIndex
+ ]
+ }},
+ #program_permissions{}, ProgramId) ->
+
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+ automate_channel_engine:listen_channel(ChannelId, { block_run_events, BlockId }),
?TRIGGERED_BY_MONITOR;
get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := <<"services.", MonitorPath/binary>>
, ?ARGUMENTS := Arguments
}},
- #program_permissions{owner_user_id=UserId}) ->
+ #program_permissions{owner_user_id=UserId}, ProgramId) ->
[ServiceId, _MonitorKey] = binary:split(MonitorPath, <<".">>),
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, MonitorId } = automate_service_registry_query:get_monitor_id(Module, UserId),
-
- case get_block_key_subkey(Arguments) of
- { key_and_subkey, Key, SubKey } ->
- automate_channel_engine:listen_channel(MonitorId, { Key, SubKey });
- { key, Key } ->
- automate_channel_engine:listen_channel(MonitorId, { Key });
- { not_found } ->
- automate_channel_engine:listen_channel(MonitorId)
- end,
- ?TRIGGERED_BY_MONITOR;
+ case automate_service_registry:get_service_by_id(ServiceId) of
+ {ok, #{ module := _Module }} ->
+ case ?UTILS:get_block_key_subkey(Arguments) of
+ { key_and_subkey, Key, SubKey } ->
+ ok = automate_service_registry_query:listen_service(ServiceId, UserId, { Key, SubKey });
+ { key, Key } ->
+ ok = automate_service_registry_query:listen_service(ServiceId, UserId, { Key, undefined });
+ { not_found } ->
+ ok = automate_service_registry_query:listen_service(ServiceId, UserId, { undefined, undefined })
+ end,
+
+ ?TRIGGERED_BY_MONITOR;
+ {error, Reason} ->
+ automate_logging:log_program_error(
+ #user_program_log_entry{ program_id=ProgramId
+ , thread_id=none
+ , owner=UserId
+ , block_id=undefined
+ , event_data={error, Reason}
+ , event_message=binary:list_to_bin(
+ lists:flatten(io_lib:format("Error finding service for signal. Might not be active anymore.", [])))
+ , event_time=erlang:system_time(millisecond)
+ , severity=warning
+ , exception_data=none
+ }),
+ false
+ end;
get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?SIGNAL_PROGRAM_CUSTOM
, ?ARGUMENTS := [ #{ ?TYPE := <<"constant">>
@@ -66,153 +156,399 @@ get_expected_action_from_trigger(#program_trigger{condition=#{ ?TYPE := ?SIGNAL_
}
, _SaveToVal
]
- }}, #program_permissions{}) ->
+ }}, #program_permissions{}, _ProgramId) ->
automate_channel_engine:listen_channel(ChannelId),
?TRIGGERED_BY_MONITOR;
%% By default let's suppose no special data is needed to keep the program running
-get_expected_action_from_trigger(Trigger, _Permissions) ->
- io:fwrite("[WARN][Bot/Triggers] Unknown trigger: ~p~n", [Trigger]),
+get_expected_action_from_trigger(Trigger, _Permissions, ProgramId) ->
+ %% io:fwrite("[WARN][Bot/Triggers][ProgId=~p] Unknown trigger: ~p~n", [ProgramId, Trigger]),
?SIGNAL_PROGRAM_TICK.
-get_block_key_subkey(#{ <<"key">> := Key
- , <<"subkey">> := #{ <<"type">> := <<"constant">>
- , <<"value">> := SubKey
- }
- }) ->
- { key_and_subkey, Key, SubKey };
-get_block_key_subkey(#{ <<"key">> := Key }) ->
- {key, Key};
-get_block_key_subkey(_) ->
- { not_found }.
-
-
-
-
%%%% Thread creation
%%% Monitors
%% If any value is OK
-spec trigger_thread(#program_trigger{}, {atom(), any()}, #program_state{}) -> 'false' | {'true', #program_thread{}}.
-trigger_thread(#program_trigger{ condition=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
- , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := MonitorId
- , ?MONITOR_EXPECTED_VALUE := ?MONITOR_ANY_VALUE
- }
+trigger_thread(Trigger=#program_trigger{ condition=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
+ , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := #{ ?FROM_SERVICE := ServiceId }
+ , ?MONITOR_EXPECTED_VALUE := ?MONITOR_ANY_VALUE
+ , ?MONITOR_KEY := MonitorKey
+ }
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, {_MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent
+ , <<"service_id">> := ServiceId
+ , ?MONITOR_KEY := MsgKey
+ }} },
+ #program_state{program_id=ProgramId}) ->
+ case MonitorKey == MsgKey of
+ true ->
+ trigger_thread_with_matching_message(Program, ProgramId, {service, ServiceId}, MonitorArgs, MessageContent, FullMessage, Trigger);
+ false ->
+ false
+ end;
+
+trigger_thread(Trigger=#program_trigger{ condition=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
+ , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := #{ ?FROM_SERVICE := ServiceId }
+ , ?MONITOR_EXPECTED_VALUE := ?MONITOR_ANY_VALUE
+ }
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, {MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent, <<"service_id">> := ServiceId }} },
+ #program_state{program_id=ProgramId}) ->
+ trigger_thread_with_matching_message(Program, ProgramId, {service, ServiceId}, MonitorArgs, MessageContent, FullMessage, Trigger);
+
+
+trigger_thread(Trigger=#program_trigger{ condition=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
+ , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := MonitorId
+ , ?MONITOR_EXPECTED_VALUE := ?MONITOR_ANY_VALUE
+ }
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, {MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent }} },
+ #program_state{program_id=ProgramId}) ->
+ trigger_thread_with_matching_message(Program, ProgramId, {channel, MonitorId}, MonitorArgs, MessageContent, FullMessage, Trigger);
+
+
+%% Special case for handling of timezone trigger
+trigger_thread(Trigger=#program_trigger{ condition=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
+ , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := #{ ?FROM_SERVICE := ?TIME_SERVICE_UUID }
+ , ?MONITOR_EXPECTED_VALUE := #{ <<"value">> := ExpectedTime }
+ , <<"timezone">> := Timezone
+ }
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, {_MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent
+ , <<"full">> := #{ <<"__as_gregorian_seconds">> := GregorianSeconds }
+ , <<"service_id">> := ServiceId
+ } } },
+ #program_state{program_id=ProgramId}) ->
+
+ %% TODO: Periodically clear this cache. Maybe when a new version is uploaded?
+ CacheKey = { internal, { time_cache, { ExpectedTime, Timezone } } },
+
+ Schedule = fun(RequireFuture) ->
+ Inc = case RequireFuture of
+ true -> 1;
+ false -> 0
+ end,
+
+ {CMegaSec, CSec, CMicroSec} = ?CORRECT_EXECUTION_TIME(erlang:timestamp()),
+
+ %% Current time in UTC
+ CurrentDateTime = calendar:now_to_datetime({CMegaSec, CSec + Inc, CMicroSec}),
+ %% In epoch-like
+ CurrentSecs = calendar:datetime_to_gregorian_seconds(CurrentDateTime),
+
+ %% Current day in Timezone
+ {Today, {_Hour, _Min, _Sec}} = qdate:to_date(Timezone, prefer_standard, calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp()))),
+ ok = qdate:set_timezone(Timezone),
+ {_, { Hour, Min, Sec }} = qdate:parse(ExpectedTime),
+ %% Goal time, in UTC
+ GoalDateTime = qdate:to_date(<<"UTC">>, {Today, {Hour, Min, Sec}}),
+ ok = qdate:set_timezone(<<"UTC">>),
+ GoalSecs = calendar:datetime_to_gregorian_seconds(GoalDateTime),
+
+ PastTodayTime = GoalSecs < CurrentSecs,
+
+ case PastTodayTime of
+ true ->
+ %% Recalculate next execution date, now for tomorrow
+ TomorrowDate = calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Today) + 1),
+
+ %% Goal time, in UTC
+ ok = qdate:set_timezone(Timezone),
+ TomorrowsGoal = qdate:to_date(<<"UTC">>, {TomorrowDate, {Hour, Min, Sec}}),
+ ok = qdate:set_timezone(<<"UTC">>),
+ calendar:datetime_to_gregorian_seconds(TomorrowsGoal);
+ false ->
+ GoalSecs
+ end
+ end,
+
+ Next = case automate_bot_engine_variables:get_program_variable(ProgramId, CacheKey) of
+ {error, not_found} ->
+ NextTime = Schedule(false),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, CacheKey, NextTime, undefined),
+ NextTime;
+ {ok, NextTime} ->
+ NextTime
+ end,
+
+ case Next =< GregorianSeconds of
+ true ->
+ Future = Schedule(true),
+ {true, Thread} = trigger_thread_with_matching_message(Program, ProgramId, {service, ServiceId},
+ MonitorArgs, MessageContent, FullMessage,
+ Trigger),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, CacheKey, Future, undefined),
+ {true, Thread};
+ false ->
+ false
+ end;
+
+%% Others, with matching value
+trigger_thread(Trigger=#program_trigger{ condition= Op=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
+ , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := #{ ?FROM_SERVICE := ServiceId }
+ , ?MONITOR_EXPECTED_VALUE := Argument
+ }
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, {MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent, <<"service_id">> := ServiceId }} },
+ #program_state{program_id=ProgramId}) ->
+ {true, Thread} = trigger_thread_with_matching_message(Program, ProgramId, {service, ServiceId}, MonitorArgs, MessageContent, FullMessage, Trigger),
+ case automate_bot_engine_variables:resolve_argument(Argument, Thread, Op) of
+ {ok, MessageContent, UpdatedThread} ->
+ {true, Thread};
+ {ok, Found, _DiscardedThread} ->
+ false
+ end;
+
+trigger_thread(Trigger=#program_trigger{ condition= Op=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
+ , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := MonitorId
+ , ?MONITOR_EXPECTED_VALUE := Argument
+ }
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, {MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent }} },
+ #program_state{program_id=ProgramId}) when is_binary(MonitorId) ->
+ {true, Thread} = trigger_thread_with_matching_message(Program, ProgramId, {channel, MonitorId}, MonitorArgs, MessageContent, FullMessage, Trigger),
+ case automate_bot_engine_variables:resolve_argument(Argument, Thread, Op) of
+ {ok, MessageContent, UpdatedThread} ->
+ {true, Thread};
+ {ok, Found, _DiscardedThread} ->
+ %% io:format("No match. Expected “~p”, found “~p”~n", [MessageContent, Found]),
+ false
+ end;
+
+%% UI channel
+trigger_thread(#program_trigger{ condition=#{ ?TYPE := <<"services.ui.", UiMonitorPath/binary>>
}
, subprogram=Program
},
- { ?TRIGGERED_BY_MONITOR, {MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent }} },
- ProgramState=#program_state{program_id=ProgramId}) ->
+ { ?TRIGGERED_BY_MONITOR, { _MonitorId
+ , #{ <<"key">> := ui_events, <<"subkey">> := UiMonitorPath, <<"value">> := Value }
+ } },
+ #program_state{ program_id=ProgramId
+ , permissions=#program_permissions{owner_user_id=_UserId}}) ->
+ #{ <<"connection">> := Source, <<"ui_data">> := UiData } = Value,
Thread = #program_thread{ position=[1]
+ , direction=forward
, program=Program
- , global_memory=#{}
+ , global_memory=#{ ?UI_TRIGGER_VALUES => #{ ?UI_TRIGGER_CONNECTION => Source, ?UI_TRIGGER_DATA => UiData } }
, instruction_memory=#{}
, program_id=ProgramId
+ , thread_id=undefined
},
+ {true, Thread};
+
+%% Monitoring changes
+trigger_thread(#program_trigger{condition=#{ ?TYPE := ?COMMAND_DATA_VARIABLE_ON_CHANGE
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := OperationVariable
+ }
+ ]
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, { _MonitorId
+ , FullMessage=#{ <<"key">> := variable_events, <<"subkey">> := ReceivedVariable }
+ } },
+ #program_state{ program_id=ProgramId
+ , permissions=#program_permissions{owner_user_id=_UserId}}) ->
+
+ %% Manage subkey canonicalization
+ case automate_channel_engine_utils:canonicalize_selector(OperationVariable) of
+ ReceivedVariable ->
+ %% Match!
+ Thread = #program_thread{ position=[1]
+ , direction=forward
+ , program=Program
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+ {true, Thread};
+ _ ->
+ false
+ end;
- {ok, ThreadWithSavedValue} = case MonitorArgs of
- #{ ?MONITOR_SAVE_VALUE_TO := SaveTo } ->
- save_value(Thread, SaveTo, MessageContent);
- _ ->
- {ok, Thread}
- end,
+trigger_thread(#program_trigger{condition=#{ ?TYPE := ?FLOW_ON_BLOCK_RUN
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := BlockId
+ }
+ , _ChangeIndex
+ ]
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, { _MonitorId
+ , FullMessage=#{ <<"key">> := block_run_events, <<"subkey">> := BlockId}
+ } },
+ #program_state{ program_id=ProgramId
+ , permissions=#program_permissions{owner_user_id=_UserId}}) ->
- {ok, NewThread} = automate_bot_engine_variables:set_last_monitor_value(
- ThreadWithSavedValue, MonitorId, FullMessage),
+ Memory = case FullMessage of
+ #{ <<"memory">> := RunMemory } ->
+ RunMemory;
+ _ ->
+ #{}
+ end,
- {true, NewThread};
+ Thread = #program_thread{ position=[1]
+ , direction=forward
+ , program=Program
+ , global_memory=Memory
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
-%% With matching value
-trigger_thread(#program_trigger{ condition=#{ ?TYPE := ?WAIT_FOR_MONITOR_COMMAND
- , ?ARGUMENTS := MonitorArgs=#{ ?MONITOR_ID := MonitorId
- , ?MONITOR_EXPECTED_VALUE := Argument
- }
- }
+ Thread2 = case FullMessage of
+ #{ <<"value">> := Value } ->
+ automate_bot_engine_variables:set_instruction_memory(Thread, Value, BlockId);
+ _ -> Thread
+ end,
+ {true, Thread2};
+
+trigger_thread(#program_trigger{condition=#{ ?TYPE := ?COMMAND_TRIGGER_ON_BRIDGE_CONNECTED
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := BridgeId
+ }
+ ]
+ }
, subprogram=Program
},
- { ?TRIGGERED_BY_MONITOR, {MonitorId, FullMessage=#{ ?CHANNEL_MESSAGE_CONTENT := MessageContent }} },
- ProgramState=#program_state{program_id=ProgramId}) ->
+ { ?TRIGGERED_BY_MONITOR, { _MonitorId
+ , #{ <<"key">> := ?PROTO_ON_BRIDGE_CONNECTED, <<"subkey">> := BridgeId }
+ } },
+ #program_state{ program_id=ProgramId
+ , permissions=#program_permissions{owner_user_id=_UserId}}) ->
Thread = #program_thread{ position=[1]
+ , direction=forward
, program=Program
, global_memory=#{}
, instruction_memory=#{}
, program_id=ProgramId
+ , thread_id=undefined
},
+ {true, Thread};
- {ok, ThreadWithSavedValue} = case MonitorArgs of
- #{ ?MONITOR_SAVE_VALUE_TO := SaveTo } ->
- save_value(Thread, SaveTo, MessageContent);
- _ ->
- {ok, Thread}
- end,
+trigger_thread(#program_trigger{condition=#{ ?TYPE := ?COMMAND_TRIGGER_ON_BRIDGE_DISCONNECTED
+ , ?ARGUMENTS := [ #{ ?TYPE := ?VARIABLE_CONSTANT
+ , ?VALUE := BridgeId
+ }
+ ]
+ }
+ , subprogram=Program
+ },
+ { ?TRIGGERED_BY_MONITOR, { _MonitorId
+ , #{ <<"key">> := ?PROTO_ON_BRIDGE_DISCONNECTED, <<"subkey">> := BridgeId }
+ } },
+ #program_state{ program_id=ProgramId
+ , permissions=#program_permissions{owner_user_id=_UserId}}) ->
- case automate_bot_engine_variables:resolve_argument(Argument, ThreadWithSavedValue) of
- {ok, MessageContent} ->
- {ok, NewThread} = automate_bot_engine_variables:set_last_monitor_value(
- ThreadWithSavedValue, MonitorId, FullMessage),
- {true, NewThread};
- {ok, Found} ->
- %% io:format("No match. Expected “~p”, found “~p”~n", [MessageContent, Found]),
- false
- end;
+ Thread = #program_thread{ position=[1]
+ , direction=forward
+ , program=Program
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+ {true, Thread};
%% Bridge channel
-trigger_thread(#program_trigger{ condition=#{ ?TYPE := <<"services.", MonitorPath/binary>>
- , ?ARGUMENTS := MonitorArgs
- }
+trigger_thread(#program_trigger{ condition= Op=#{ ?TYPE := <<"services.", MonitorPath/binary>>
+ , ?ARGUMENTS := MonitorArgs
+ }
, subprogram=Program
},
- { ?TRIGGERED_BY_MONITOR, { TriggeredMonitorId
- , FullMessage=#{ <<"key">> := TriggeredKey }
+ { ?TRIGGERED_BY_MONITOR, { MonitorId
+ , FullMessage=#{ <<"key">> := TriggeredKey, <<"service_id">> := BridgeId }
} },
- ProgramState=#program_state{ program_id=ProgramId
- , permissions=#program_permissions{owner_user_id=UserId}}) ->
+ #program_state{ program_id=ProgramId
+ , permissions=#program_permissions{owner_user_id=_UserId}}) ->
Thread = #program_thread{ position=[1]
+ , direction=forward
, program=Program
, global_memory=#{}
, instruction_memory=#{}
, program_id=ProgramId
+ , thread_id=undefined
},
[ServiceId, FunctionName] = binary:split(MonitorPath, <<".">>),
- MonitorKey = case MonitorArgs of
- #{ <<"key">> := Key } ->
- Key;
- _ ->
- FunctionName
- end,
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, MonitorId } = automate_service_registry_query:get_monitor_id(Module, UserId),
- MatchingContent = case MonitorArgs of
- #{ ?MONITOR_EXPECTED_VALUE := ExpectedValue } ->
- {ok, ResolvedExpectedValue} = automate_bot_engine_variables:resolve_argument(
- ExpectedValue, Thread),
- ActualValue = maps:get(?CHANNEL_MESSAGE_CONTENT, FullMessage, none),
- ResolvedExpectedValue == ActualValue;
- _ ->
- true
- end,
- case {MonitorId, MonitorKey, MatchingContent} of
- {TriggeredMonitorId, TriggeredKey, true} ->
- {ok, ThreadWithSavedValue} = case {MonitorArgs, FullMessage} of
- { #{ ?MONITOR_SAVE_VALUE_TO := SaveTo }
- , #{ ?CHANNEL_MESSAGE_CONTENT := MessageContent }
- } ->
- save_value(Thread, SaveTo, MessageContent);
+
+ KeyMatch = case ?UTILS:get_block_key_subkey(MonitorArgs) of
+ { key_and_subkey, Key, SubKey } ->
+ case ?UTILS:get_subkey_value(FullMessage) of
+ {ok, TriggeredSubKey} ->
+ (Key == TriggeredKey) and (string:lowercase(SubKey) == string:lowercase(TriggeredSubKey));
+ _ ->
+ false
+ end;
+ { key, Key } ->
+ Key == TriggeredKey;
+ { not_found } ->
+ FunctionName == TriggeredKey
+ end,
+
+ case KeyMatch and (BridgeId == ServiceId) of
+ false ->
+ false;
+ true ->
+ {MatchingContent, Thread2} = case MonitorArgs of
+ #{ ?MONITOR_EXPECTED_VALUE := ExpectedValue } ->
+ {ok, ResolvedExpectedValue, UpdatedThread} = automate_bot_engine_variables:resolve_argument(
+ ExpectedValue, Thread, Op),
+ ActualValue = maps:get(?CHANNEL_MESSAGE_CONTENT, FullMessage, none),
+ {ResolvedExpectedValue == ActualValue, UpdatedThread};
_ ->
- {ok, Thread}
+ {true, Thread}
end,
-
- {ok, NewThread} = automate_bot_engine_variables:set_last_monitor_value(
- ThreadWithSavedValue, MonitorId, FullMessage),
- {true, NewThread};
- _ ->
- false
+ case MatchingContent of
+ true ->
+ {ok, ThreadWithSavedValue} = case {MonitorArgs, FullMessage} of
+ { #{ ?MONITOR_SAVE_VALUE_TO := SaveTo }
+ , #{ ?CHANNEL_MESSAGE_CONTENT := MessageContent }
+ } ->
+ save_value(Thread2, SaveTo, MessageContent);
+ _ ->
+ {ok, Thread2}
+ end,
+
+ {ok, NewThread} = automate_bot_engine_variables:set_last_bridge_value(
+ ThreadWithSavedValue, ServiceId, FullMessage),
+
+ SavedThread = case {?UTILS:get_block_id(Op), FullMessage} of
+ {undefined, _} ->
+ NewThread;
+ {BlockId, #{ ?CHANNEL_MESSAGE_CONTENT := Content }} ->
+ automate_bot_engine_variables:set_instruction_memory(NewThread,
+ Content,
+ BlockId);
+ _ ->
+ NewThread
+ end,
+
+ {true, SavedThread};
+ _ ->
+ false
+ end
end;
%% Custom trigger
@@ -231,10 +567,12 @@ trigger_thread(#program_trigger{ condition=#{ ?TYPE := ?SIGNAL_PROGRAM_CUSTOM
#program_state{ program_id=ProgramId }) ->
Thread = #program_thread{ position=[1]
+ , direction=forward
, program=Program
, global_memory=#{}
, instruction_memory=#{}
, program_id=ProgramId
+ , thread_id=undefined
},
case SaveTo of
@@ -250,15 +588,58 @@ trigger_thread(Trigger, Message, ProgramState) ->
notify_trigger_not_matched(Trigger, Message, ProgramState),
false.
-
%%%===================================================================
%%% Aux functions
%%%===================================================================
+trigger_thread_with_matching_message(Program, ProgramId, Channel, MonitorArgs, MessageContent, FullMessage,
+ #program_trigger{condition=Condition}) ->
+ Thread = #program_thread{ position=[1]
+ , direction=forward
+ , program=Program
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
-save_value(Thread, #{ ?TYPE := ?VARIABLE_VARIABLE
- , ?VALUE := VariableName
- }, Value) ->
- automate_bot_engine_variables:set_program_variable(Thread, VariableName, Value).
+ {ok, ThreadWithSavedValue} = case MonitorArgs of
+ #{ ?MONITOR_SAVE_VALUE_TO := SaveTo } ->
+ save_value(Thread, SaveTo, MessageContent);
+ _ ->
+ {ok, Thread}
+ end,
+ Thread2 = case Channel of
+ {channel, ChannelId} ->
+ case automate_service_port_engine:get_channel_origin_bridge(ChannelId) of
+ {ok, ServiceId} ->
+ {ok, Thread1} = automate_bot_engine_variables:set_last_bridge_value(
+ ThreadWithSavedValue, ServiceId, FullMessage),
+ Thread1;
+ {error, not_found} ->
+ Thread
+ end;
+ {service, ServiceId} ->
+ {ok, Thread1} = automate_bot_engine_variables:set_last_bridge_value(
+ ThreadWithSavedValue, ServiceId, FullMessage),
+ Thread1
+ end,
+ NewThread = case Condition of
+ #{ ?BLOCK_ID := BlockId } ->
+ automate_bot_engine_variables:set_instruction_memory(
+ Thread2, FullMessage, BlockId);
+ _ ->
+ Thread2
+ end,
+
+ {true, NewThread}.
+
+
+save_value(Thread=#program_thread{ program_id=ProgramId }
+ , #{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := VariableName
+ }, Value) ->
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, VariableName, Value, undefined),
+ {ok, Thread}.
-ifdef(TEST).
@@ -267,7 +648,7 @@ notify_trigger_not_matched(_Trigger, { triggered_by_monitor
_Program) ->
ok;
notify_trigger_not_matched(_Trigger, {tick, _}, _Program) ->
- io:format(" *tick* ");
+ ok;
notify_trigger_not_matched(Trigger, Message, _Program) ->
io:format("Trigger (~p) not matching (~p) ~n", [Message, Trigger]).
-else.
@@ -278,7 +659,7 @@ notify_trigger_not_matched(_Trigger, { triggered_by_monitor
notify_trigger_not_matched(_Trigger, {tick, _}, _Program) ->
ok;
%% notify_trigger_not_matched(Trigger, Message, _Program) ->
-%% io:format("Trigger (~p) not matching (~p) ~n", [Message, Trigger]).
+%% io:format("Trigger (~p) not matching (~p) ~n", [Message, Trigger]);
notify_trigger_not_matched(_Trigger, _Message, _Program) ->
ok.
-endif.
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_utils.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_utils.erl
new file mode 100644
index 00000000..c3bb568d
--- /dev/null
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_utils.erl
@@ -0,0 +1,29 @@
+-module(automate_bot_engine_utils).
+
+-export([ get_block_id/1
+ , get_block_key_subkey/1
+ , get_subkey_value/1
+ ]).
+
+-include("instructions.hrl").
+
+get_block_id(#{ ?BLOCK_ID := BlockId }) ->
+ BlockId;
+get_block_id(_) ->
+ none.
+
+get_block_key_subkey(#{ <<"key">> := Key
+ , <<"subkey">> := #{ <<"type">> := <<"constant">>
+ , <<"value">> := SubKey
+ }
+ }) ->
+ { key_and_subkey, Key, SubKey };
+get_block_key_subkey(#{ <<"key">> := Key }) ->
+ {key, Key};
+get_block_key_subkey(_) ->
+ { not_found }.
+
+get_subkey_value(#{ <<"subkey">> := SubKey }) when is_binary(SubKey) ->
+ {ok, SubKey};
+get_subkey_value(_) ->
+ {error, not_found}.
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_values.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_values.erl
index 41dc231c..d180edaa 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_values.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_values.erl
@@ -7,11 +7,18 @@
, subtract/2
, multiply/2
, divide/2
+ , modulo/2
, is_less_than/2
, is_greater_than/2
+ , are_equal/1
, is_equal_to/2
+ , string_contains/2
+ , check_variable_safety/1
]).
+-include("../../automate_common_types/src/limits.hrl").
+-include("program_records.hrl").
+
%%%===================================================================
%%% API
%%%===================================================================
@@ -22,8 +29,8 @@ add(Left, Right) when is_binary(Left) and is_binary(Right) ->
{ok, PreviousInt + ChangeInt};
{float, PreviousF, ChangeF} ->
{ok, PreviousF + ChangeF};
- {string, PreviousS, ChangeS} ->
- {error, not_found}
+ {string, LeftS, RightS} ->
+ join(LeftS, RightS)
end;
%% If everything else failed, just do simple concatenation
@@ -31,10 +38,12 @@ add(V1, V2) ->
add(to_bin(V1), to_bin(V2)).
-spec join(_, _) -> {ok, binary()}.
+join(V1, V2) when is_binary(V1) and is_binary(V2) ->
+ {ok, <>};
join(V1, V2) ->
- {ok, binary:list_to_bin(lists:flatten(io_lib:format("~s~s", [to_string(V1), to_string(V2)])))}.
+ {ok, binary:list_to_bin(io_lib:format("~s~s", [to_string(V1), to_string(V2)]))}.
--spec get_value_by_key(binary(), map()) -> {ok, binary()}.
+-spec get_value_by_key(binary(), map()) -> {ok, binary()} | {error, not_found}.
get_value_by_key(Key, Map) when is_map(Map) and is_binary(Key) ->
case maps:is_key(Key, Map) of
true -> {ok, maps:get(Key, Map)};
@@ -42,7 +51,7 @@ get_value_by_key(Key, Map) when is_map(Map) and is_binary(Key) ->
end;
%% If this is not a map, fail
-get_value_by_key(V1, V2) ->
+get_value_by_key(_V1, _V2) ->
{error, not_found}.
-spec subtract(_, _) -> {ok, number()} | {error, not_found}.
@@ -89,6 +98,22 @@ divide(Left, Right) when is_binary(Left) and is_binary(Right) ->
divide(V1, V2) ->
divide(to_bin(V1), to_bin(V2)).
+-spec modulo(_, _) -> {ok, number()} | {error, not_found}.
+modulo(Left, Right) when is_binary(Left) and is_binary(Right) ->
+ case combined_type(Left, Right) of
+ {integer, PreviousInt, ChangeInt} ->
+ %% Probably overkill, but this implements a proper modulo, and not
+ %% just a truncated division.
+ {ok, math:fmod(math:fmod(PreviousInt, ChangeInt) + ChangeInt, ChangeInt)};
+ {float, PreviousF, ChangeF} ->
+ {ok, math:fmod(math:fmod(PreviousF, ChangeF) + ChangeF, ChangeF)};
+ _ ->
+ {error, not_found}
+ end;
+
+modulo(V1, V2) ->
+ modulo(to_bin(V1), to_bin(V2)).
+
-spec is_less_than(_, _) -> {ok, boolean()} | {error, not_found}.
is_less_than(V1, V2) when is_binary(V1) and is_binary(V2) ->
@@ -118,6 +143,23 @@ is_greater_than(V1, V2) when is_binary(V1) and is_binary(V2) ->
is_greater_than(V1, V2) ->
is_greater_than(to_bin(V1), to_bin(V2)).
+
+%% Probably this can be optimized to perform combined_type/2 less times, but
+%% this should work for now.
+
+%% NOTE there might be some cases where evaluating combined_type/2 independently
+%% might cause strange comparation bugs to happen. If one is found, strongly
+%% consider rethinking this function.
+-spec are_equal([any]) -> {ok, boolean()}.
+are_equal(Values=[_|T]) ->
+ Pairs = lists:zip(lists:droplast(Values), T),
+ { ok
+ , lists:all(fun({X, Y}) ->
+ {ok, Result} = is_equal_to(X, Y),
+ Result
+ end, Pairs)
+ }.
+
-spec is_equal_to(_, _) -> {ok, boolean()} | {error, not_found}.
is_equal_to(V1, V2) when is_binary(V1) and is_binary(V2) ->
case combined_type(V1, V2) of
@@ -132,6 +174,15 @@ is_equal_to(V1, V2) when is_binary(V1) and is_binary(V2) ->
is_equal_to(V1, V2) ->
is_equal_to(to_bin(V1), to_bin(V2)).
+-spec string_contains(_, _) -> {ok, boolean()} | {error, not_found}.
+string_contains(Haystack, Needle) when is_binary(Haystack) and is_binary(Needle) ->
+ CHaystack = canonicalize_string(Haystack),
+ CNeedle = canonicalize_string(Needle),
+ Result = string:find(CHaystack, CNeedle) =/= nomatch,
+ {ok, Result};
+string_contains(Haystack, Needle) ->
+ string_contains(to_bin(Haystack), to_bin(Needle)).
+
%%%===================================================================
%%% Type handling methods
@@ -161,9 +212,9 @@ combined_type(V1, V2) when is_binary(V1) and is_binary(V2) ->
end
end;
-%% If everything else failed, just do simple concatenation
+%% Map all to strings if they are not already
combined_type(V1, V2) ->
- {string, io_lib:format("~p", [V1]), io_lib:format("~p", [V2])}.
+ {string, to_bin(V1), to_bin(V2)}.
to_int(Value) when is_binary(Value) ->
case string:to_integer(Value) of
@@ -189,3 +240,41 @@ to_float(Value) when is_binary(Value) ->
X
end
end.
+
+%% Convert non-ascii characters to their closest ones.
+canonicalize_string(Str) when is_binary(Str) ->
+
+ Uni = unicode:characters_to_binary(Str, utf8), % This corrects non-unicode binaries. For example the ones containing 'ó' pairs instead of 'ó'
+ Decomposed = unicode:characters_to_nfkd_list(Uni),
+ Filtered = lists:filter(fun(X) -> X < 256 end, Decomposed), % Remove non-ascii characters
+
+ %% Unify casing and convert back from list to binary.
+ list_to_binary(string:casefold(Filtered)).
+
+
+%%%===================================================================
+%%% Safety checks
+%%%===================================================================
+-spec check_variable_safety(_) -> ok | {error, {safety_error, #memory_item_size_exceeded{}}}.
+check_variable_safety(Var) ->
+ check_variable_size_safety(Var).
+
+check_variable_size_safety(Var) ->
+ Size = get_var_size(Var),
+ case Size < ?USER_PROGRAM_MAX_VAR_SIZE of
+ true ->
+ ok;
+ false ->
+ {error, {safety_error, #memory_item_size_exceeded{ next_size=Size, max_size=?USER_PROGRAM_MAX_VAR_SIZE }}}
+ end.
+
+%% Do note that this is very approximate, should not be trusted more than for simple tests.
+-spec get_var_size(_) -> pos_integer().
+get_var_size(B) when is_binary(B) ->
+ size(B);
+get_var_size(L) when is_list(L) ->
+ lists:foldl(fun(E, Acc) -> get_var_size(E) + Acc end, 0, L);
+get_var_size(M) when is_map(M) ->
+ lists:foldl(fun({K, V}, Acc) -> get_var_size(K) + get_var_size(V) + Acc end, 0, maps:to_list(M));
+get_var_size(_) ->
+ erlang:system_info(wordsize).
diff --git a/backend/apps/automate_bot_engine/src/automate_bot_engine_variables.erl b/backend/apps/automate_bot_engine/src/automate_bot_engine_variables.erl
index 04414d18..7225c2ab 100644
--- a/backend/apps/automate_bot_engine/src/automate_bot_engine_variables.erl
+++ b/backend/apps/automate_bot_engine/src/automate_bot_engine_variables.erl
@@ -1,27 +1,32 @@
-module(automate_bot_engine_variables).
%% API
--export([ resolve_argument/2
+-export([ resolve_argument/3
, set_thread_value/3
, get_program_variable/2
- , set_program_variable/3
+ , set_program_variable/4
+ , delete_program_variable/2
- , set_last_monitor_value/3
- , get_last_monitor_value/2
+ , set_last_bridge_value/3
+ , get_last_bridge_value/2
, retrieve_thread_value/2
, retrieve_thread_values/2
, retrieve_instruction_memory/1
+ , retrieve_instruction_memory/2
, set_instruction_memory/2
+ , set_instruction_memory/3
, unset_instruction_memory/1
+ , get_thread_context/1
]).
-define(SERVER, ?MODULE).
-include("../../automate_storage/src/records.hrl").
-include("program_records.hrl").
-include("instructions.hrl").
+-define(UTILS, automate_bot_engine_utils).
%%%===================================================================
%%% API
@@ -33,29 +38,52 @@
%% }) ->
%% automate_monitor_engine:get_last_monitor_result(MonitorId).
--spec resolve_argument(map(), #program_thread{}) -> {ok, any()} | {error, not_found}.
+-spec resolve_argument(map(), #program_thread{}, map()) -> {ok, any(), #program_thread{}} | {error, not_found}.
resolve_argument(#{ ?TYPE := ?VARIABLE_CONSTANT
, ?VALUE := Value
- }, _Thread) ->
- {ok, Value};
+ }, Thread, _ParentBlock) ->
+ {ok, Value, Thread};
resolve_argument(#{ ?TYPE := ?VARIABLE_BLOCK
, ?VALUE := [Operator]
- }, Thread) ->
+ }, Thread, _ParentBlock) ->
automate_bot_engine_operations:get_result(Operator, Thread);
-resolve_argument(#{ ?TYPE := ?VARIABLE_VARIABLE
- , ?VALUE := VariableName
- }, Thread) ->
- get_program_variable(Thread, VariableName);
+resolve_argument(Op=#{ ?TYPE := ?VARIABLE_VARIABLE
+ , ?VALUE := VariableName
+ }, Thread, ParentBlock) ->
+ case get_program_variable(Thread, VariableName) of
+ {ok, Value} ->
+ {ok, Value, Thread};
+ {error, not_found} ->
+ BlockId = case {?UTILS:get_block_id(Op), ?UTILS:get_block_id(ParentBlock)} of
+ { none, Value } -> Value;
+ { Value, _ } -> Value
+ end,
-resolve_argument(#{ ?TYPE := ?VARIABLE_LIST
- , ?VALUE := VariableName
- }, Thread) ->
- get_program_variable(Thread, VariableName).
+ throw(#program_error{ error=#variable_not_set{ variable_name=VariableName }
+ , block_id=BlockId
+ })
+ end;
+resolve_argument(Op=#{ ?TYPE := ?VARIABLE_LIST
+ , ?VALUE := VariableName
+ }, Thread, ParentBlock) ->
+ case get_program_variable(Thread, VariableName) of
+ {ok, Value} ->
+ {ok, Value, Thread};
+ {error, not_found} ->
+ BlockId = case {?UTILS:get_block_id(Op), ?UTILS:get_block_id(ParentBlock)} of
+ { none, Value } -> Value;
+ { Value, _ } -> Value
+ end,
--spec retrieve_thread_value(#program_thread{}, atom()) -> {ok, any()} | {error, any()}.
+ throw(#program_error{ error=#list_not_set{ list_name=VariableName }
+ , block_id=BlockId
+ })
+ end.
+
+-spec retrieve_thread_value(#program_thread{}, atom() | binary()) -> {ok, any()} | {error, any()}.
retrieve_thread_value(#program_thread{ global_memory=Global }, Key) ->
case maps:find(Key, Global) of
Response = {ok, _} ->
@@ -64,33 +92,49 @@ retrieve_thread_value(#program_thread{ global_memory=Global }, Key) ->
{error, not_found}
end.
--spec retrieve_thread_values(#program_thread{}, [atom()]) -> {ok, [any()]} | {error, any()}.
+-spec retrieve_thread_values(#program_thread{}, [atom() | binary()]) -> {ok, [any()]} | {error, any()}.
retrieve_thread_values(Thread, Keys) ->
retrieve_thread_values(Thread, Keys, []).
--spec set_thread_value(#program_thread{}, atom() | [binary()], any()) -> {ok, #program_thread{}}.
+-spec set_thread_value(#program_thread{}, binary() | [binary()], any()) -> {ok, #program_thread{}}.
set_thread_value(Thread = #program_thread{}, Key, Value) when is_list(Key) ->
set_thread_nested_value(Thread, Key, Value);
set_thread_value(Thread = #program_thread{ global_memory=Global }, Key, Value) ->
{ok, Thread#program_thread{ global_memory=Global#{ Key => Value } } }.
--spec set_program_variable(#program_thread{}, atom(), any()) -> {ok, #program_thread{}}.
-set_program_variable(Thread = #program_thread{ program_id=ProgramId }, Key, Value) ->
- ok = automate_storage:set_program_variable(ProgramId, Key, Value),
- {ok, Thread}.
+-spec set_program_variable(binary(), binary() | {internal, _}, any(), undefined | binary()) -> ok | {error, _}.
+set_program_variable(ProgramId, Key, Value, BlockId) ->
+ case automate_bot_engine_values:check_variable_safety(Value) of
+ ok ->
+ ok = automate_storage:set_program_variable(ProgramId, Key, Value),
+ notify_variable_update(Key, ProgramId, Value);
+ {error, { safety_error, Error }} ->
+ throw(#program_error{error=Error, block_id=BlockId})
+ end.
+
+-spec delete_program_variable(binary(), binary()) -> ok | {error, _}.
+delete_program_variable(ProgramId, Key) ->
+ automate_storage:delete_program_variable(ProgramId, Key).
--spec get_program_variable(#program_thread{}, atom()) -> {ok, any()} | {error, not_found}.
+-spec get_program_variable(#program_thread{} | binary(), binary() | {internal, _}) -> {ok, any()} | {error, not_found}.
get_program_variable(#program_thread{ program_id=ProgramId }, Key) ->
- automate_storage:get_program_variable(ProgramId, Key).
+ get_program_variable(ProgramId, Key);
+get_program_variable(ProgramId, Key) when is_binary(ProgramId) ->
+ case automate_storage:get_program_variable(ProgramId, Key) of
+ {ok, Value} ->
+ {ok, Value};
+ {error, not_found} ->
+ {error, not_found}
+ end.
--spec set_last_monitor_value(#program_thread{}, binary(), any()) -> {ok, #program_thread{}}.
-set_last_monitor_value(Thread, MonitorId, Value) ->
- set_thread_value(Thread, [?LAST_MONITOR_VALUES, MonitorId], Value).
+-spec set_last_bridge_value(#program_thread{}, binary(), any()) -> {ok, #program_thread{}}.
+set_last_bridge_value(Thread, BridgeId, Value) ->
+ set_thread_value(Thread, [?LAST_BRIDGE_VALUES, BridgeId], Value).
--spec get_last_monitor_value(#program_thread{}, binary()) -> {ok, any()} | {error, not_found}.
-get_last_monitor_value(Thread, MonitorId) ->
- get_thread_value(Thread, [?LAST_MONITOR_VALUES, MonitorId]).
+-spec get_last_bridge_value(#program_thread{}, binary()) -> {ok, any()} | {error, not_found}.
+get_last_bridge_value(Thread, BridgeId) ->
+ get_thread_value(Thread, [?LAST_BRIDGE_VALUES, BridgeId]).
-spec retrieve_instruction_memory(#program_thread{}) -> {ok, any()} | {error, not_found}.
retrieve_instruction_memory(#program_thread{ instruction_memory=Memory, position=Position }) ->
@@ -101,14 +145,31 @@ retrieve_instruction_memory(#program_thread{ instruction_memory=Memory, position
{error, not_found}
end.
+-spec retrieve_instruction_memory(#program_thread{}, any()) -> {ok, any()} | {error, not_found}.
+retrieve_instruction_memory(#program_thread{ instruction_memory=Memory }, Position) ->
+ case maps:find(Position, Memory) of
+ Response = {ok, _} ->
+ Response;
+ error ->
+ {error, not_found}
+ end.
+
-spec set_instruction_memory(#program_thread{}, any()) -> #program_thread{}.
set_instruction_memory(Thread=#program_thread{ instruction_memory=Memory, position=Position }, Value) ->
Thread#program_thread{ instruction_memory=Memory#{ Position => Value } }.
+-spec set_instruction_memory(#program_thread{}, any(), any()) -> #program_thread{}.
+set_instruction_memory(Thread=#program_thread{ instruction_memory=Memory }, Value, Position) ->
+ Thread#program_thread{ instruction_memory=Memory#{ Position => Value } }.
+
-spec unset_instruction_memory(#program_thread{}) -> #program_thread{}.
unset_instruction_memory(Thread=#program_thread{ instruction_memory=Memory, position=Position }) ->
Thread#program_thread{ instruction_memory=maps:remove(Position, Memory) }.
+-spec get_thread_context(#program_thread{}) -> {ok, map()}.
+get_thread_context(Thread=#program_thread{ instruction_memory=Memory, position=Position }) ->
+ get_context_from_memory(Memory, Position, #{}).
+
%%%===================================================================
%%% Internal values
%%%===================================================================
@@ -159,3 +220,60 @@ get_memory([H | T], Mem) ->
_ ->
{error, not_found}
end.
+
+get_context_from_memory(_Memory, [], Acc) ->
+ {ok, Acc};
+get_context_from_memory(Memory, Position, Acc) ->
+ InstructionMemory = case maps:find(Position, Memory) of
+ {ok, Result} ->
+ Result;
+ error ->
+ []
+ end,
+ get_context_from_memory(Memory, lists:droplast(Position), add_to_context_acc(InstructionMemory, Acc)).
+
+add_to_context_acc([], Context) ->
+ Context;
+add_to_context_acc([{ context_group, Key, { SubKey, SubValue } } | T], Context) ->
+ PrevValue = case maps:is_key(Key, Context) of
+ true -> maps:get(Key, Context);
+ false -> #{}
+ end,
+ case maps:is_key(SubKey, PrevValue) of
+ true ->
+ %% Repeated key, as we're going bottom-up, we don't update
+ %% the context. This way we get the innermost values, which are
+ %% the ones to be used.
+ add_to_context_acc(T, Context);
+ false ->
+ add_to_context_acc(T, Context#{ Key => PrevValue#{ SubKey => SubValue } })
+ end;
+add_to_context_acc([{ context, Key, Value } | T], Context) ->
+ case maps:is_key(Key, Context) of
+ true ->
+ %% Repeated key, as we're going bottom-up, we don't update
+ %% the context. This way we get the innermost values, which are
+ %% the ones to be used.
+ add_to_context_acc(T, Context);
+ false ->
+ add_to_context_acc(T, Context#{ Key => Value })
+ end;
+add_to_context_acc([ _ | T ], Context) ->
+ add_to_context_acc(T, Context);
+add_to_context_acc(_, Context) ->
+ Context.
+
+-spec notify_variable_update(VariableName :: binary() | { internal, _ }, binary(), _) -> ok | {error, _}.
+notify_variable_update({internal, _ }, _ProgramId, _Value) ->
+ ok; %% Unused at this point
+notify_variable_update(VariableName, ProgramId, Value) ->
+ case automate_storage:get_program_from_id(ProgramId) of
+ {ok, #user_program_entry{ program_channel=ChannelId }} ->
+ automate_channel_engine:send_to_channel(ChannelId, #{ <<"key">> => variable_events
+ %% This canonicalization is done also on the channel engine, but it's not saved to the subkey
+ , <<"subkey">> => automate_channel_engine_utils:canonicalize_selector(VariableName)
+ , <<"value">> => Value
+ });
+ {error, Reason} ->
+ {error, Reason}
+ end.
diff --git a/backend/apps/automate_bot_engine/src/databases.hrl b/backend/apps/automate_bot_engine/src/databases.hrl
new file mode 100644
index 00000000..8f73173a
--- /dev/null
+++ b/backend/apps/automate_bot_engine/src/databases.hrl
@@ -0,0 +1,14 @@
+-include("../../automate_storage/src/databases.hrl").
+
+-define(BOT_REQUIRED_DBS, [ ?USER_PROGRAMS_TABLE
+ , ?RUNNING_PROGRAMS_TABLE
+ , ?RUNNING_THREADS_TABLE
+ , ?PROGRAM_VARIABLE_TABLE
+ ]).
+-define(BOT_EXTRA_DBS, [ ?PROGRAM_TAGS_TABLE
+ , ?USER_PROGRAM_LOGS_TABLE
+ , ?USER_GENERATED_LOGS_TABLE
+ , ?USER_PROGRAM_EVENTS_TABLE
+ , ?USER_PROGRAM_CHECKPOINTS_TABLE
+ , ?CUSTOM_SIGNALS_TABLE
+ ]).
diff --git a/backend/apps/automate_bot_engine/src/instructions.hrl b/backend/apps/automate_bot_engine/src/instructions.hrl
index dc2108f8..dda5ad8e 100644
--- a/backend/apps/automate_bot_engine/src/instructions.hrl
+++ b/backend/apps/automate_bot_engine/src/instructions.hrl
@@ -1,62 +1,13 @@
+-include("operation_instructions.hrl").
+
%%%% Instruction map entry names
-define(TYPE, <<"type">>).
-define(ARGUMENTS, <<"args">>).
-define(CONTENTS, <<"contents">>).
--define(ID, <<"id">>).
+-define(BLOCK_ID, <<"id">>).
-define(VALUE, <<"value">>).
-define(TEMPLATE_NAME_TYPE, <<"constant">>).
-
-%%%% Command types
-%%%% Operations
-%% Call service
--define(COMMAND_CALL_SERVICE, <<"command_call_service">>).
-
-%% General control
--define(COMMAND_WAIT, <<"control_wait">>).
--define(COMMAND_REPEAT, <<"control_repeat">>).
--define(COMMAND_REPEAT_UNTIL, <<"control_repeat_until">>).
--define(COMMAND_WAIT_UNTIL, <<"control_wait_until">>).
--define(COMMAND_IF, <<"control_if">>).
--define(COMMAND_IF_ELSE, <<"control_if_else">>).
-
-%% String operations
--define(COMMAND_JOIN, <<"operator_join">>).
--define(COMMAND_JSON, <<"operator_json_parser">>).
-
-%% Templates
--define(MATCH_TEMPLATE_STATEMENT, <<"automate_match_template_stmt">>).
--define(MATCH_TEMPLATE_CHECK, <<"automate_match_template_check">>).
-
-%% Any() operations
--define(COMMAND_EQUALS, <<"operator_equals">>).
--define(COMMAND_LESS_THAN, <<"operator_lt">>).
--define(COMMAND_GREATER_THAN, <<"operator_gt">>).
-
-%% Boolean operations
--define(COMMAND_AND, <<"operator_and">>).
--define(COMMAND_OR, <<"operator_or">>).
--define(COMMAND_NOT, <<"operator_not">>).
-
-%% Numeric operations
--define(COMMAND_ADD, <<"operator_add">>).
--define(COMMAND_SUBTRACT, <<"operator_subtract">>).
--define(COMMAND_MULTIPLY, <<"operator_multiply">>).
--define(COMMAND_DIVIDE, <<"operator_divide">>).
-
-%% Variable control
--define(COMMAND_SET_VARIABLE, <<"data_setvariableto">>).
--define(COMMAND_CHANGE_VARIABLE, <<"data_changevariableby">>).
--define(COMMAND_DATA_VARIABLE, <<"data_variable">>).
-
-%% List control
--define(COMMAND_ADD_TO_LIST, <<"data_addtolist">>).
--define(COMMAND_DELETE_OF_LIST, <<"data_deleteoflist">>).
--define(COMMAND_INSERT_AT_LIST, <<"data_insertatlist">>).
--define(COMMAND_REPLACE_VALUE_AT_INDEX, <<"data_replaceitemoflist">>).
--define(COMMAND_ITEM_OF_LIST, <<"data_itemoflist">>).
--define(COMMAND_ITEMNUM_OF_LIST, <<"data_itemnumoflist">>).
--define(COMMAND_LENGTH_OF_LIST, <<"data_lengthoflist">>).
--define(COMMAND_LIST_CONTAINS_ITEM, <<"data_listcontainsitem">>).
+-define(REPORT_STATE, <<"report_state">>).
%%%% Variables
-define(VARIABLE_BLOCK, <<"block">>).
@@ -72,7 +23,12 @@
-define(COMMAND_CUSTOM_SIGNAL, <<"automate_trigger_custom_signal">>). % Caller
%%%% Operation parameters
+-ifdef(NOTEST).
-define(MILLIS_PER_TICK, 100).
+-else.
+-define(MILLIS_PER_TICK, 1).
+-endif.
+
%%%% Monitors
%% Values
@@ -85,10 +41,18 @@
-define(MONITOR_ID, <<"monitor_id">>).
-define(MONITOR_EXPECTED_VALUE, <<"monitor_expected_value">>).
-define(MONITOR_SAVE_VALUE_TO, <<"monitor_save_value_to">>).
+-define(MONITOR_KEY, <<"key">>).
+-define(FROM_SERVICE, <<"from_service">>).
%%%% Services
-define(SERVICE_ID, <<"service_id">>).
-define(SERVICE_CALL_VALUES, <<"service_call_values">>).
-define(SERVICE_ACTION, <<"service_action">>).
--define(LAST_MONITOR_VALUES, <<"__last_monitor_values__">>).
+-define(LAST_BRIDGE_VALUES, <<"__last_bridge_values__">>).
+-define(UI_TRIGGER_VALUES, <<"__ui_trigger_values">>).
+-define(UI_TRIGGER_CONNECTION, <<"connection">>).
+-define(UI_TRIGGER_DATA, <<"ui_data">>).
+
+%%%% Special data
+-define(LIST_FILL, null).
diff --git a/backend/apps/automate_bot_engine/src/operation_instructions.hrl b/backend/apps/automate_bot_engine/src/operation_instructions.hrl
new file mode 100644
index 00000000..f736d0d8
--- /dev/null
+++ b/backend/apps/automate_bot_engine/src/operation_instructions.hrl
@@ -0,0 +1,77 @@
+%%%% Command types
+%%%% Operations
+%% Call service
+-define(COMMAND_CALL_SERVICE, <<"command_call_service">>).
+-define(CONTEXT_SELECT_CONNECTION, <<"operator_select_connection">>).
+
+%% General control
+-define(COMMAND_WAIT, <<"control_wait">>).
+-define(COMMAND_REPEAT, <<"control_repeat">>).
+-define(COMMAND_REPEAT_UNTIL, <<"control_repeat_until">>).
+-define(COMMAND_WAIT_UNTIL, <<"control_wait_until">>).
+-define(COMMAND_IF, <<"control_if">>).
+-define(COMMAND_IF_ELSE, <<"control_if_else">>).
+-define(COMMAND_FORK_EXECUTION, <<"op_fork_execution">>).
+-define(OP_FORK_CONTINUE_ON_FIRST, <<"exit-when-first-completed">>).
+-define(COMMAND_WAIT_FOR_NEXT_VALUE, <<"control_wait_for_next_value">>).
+-define(COMMAND_SIGNAL_WAIT_FOR_PULSE, <<"control_signal_wait_for_pulse">>).
+-define(COMMAND_BROADCAST_TO_ALL_USERS, <<"control_broadcast_to_all_users">>).
+-define(COMMAND_TRIGGER_ON_BRIDGE_CONNECTED, <<"trigger_on_bridge_connected">>).
+-define(COMMAND_TRIGGER_ON_BRIDGE_DISCONNECTED, <<"trigger_on_bridge_disconnected">>).
+
+%% String operations
+-define(COMMAND_JOIN, <<"operator_join">>).
+-define(COMMAND_STRING_CONTAINS, <<"operator_contains">>).
+-define(COMMAND_JSON, <<"operator_json_parser">>).
+
+%% Templates
+-define(MATCH_TEMPLATE_STATEMENT, <<"automate_match_template_stmt">>).
+-define(MATCH_TEMPLATE_CHECK, <<"automate_match_template_check">>).
+
+%% Any() operations
+-define(COMMAND_EQUALS, <<"operator_equals">>).
+-define(COMMAND_LESS_THAN, <<"operator_lt">>).
+-define(COMMAND_GREATER_THAN, <<"operator_gt">>).
+
+%% Boolean operations
+-define(COMMAND_AND, <<"operator_and">>).
+-define(COMMAND_OR, <<"operator_or">>).
+-define(COMMAND_NOT, <<"operator_not">>).
+
+%% Numeric operations
+-define(COMMAND_ADD, <<"operator_add">>).
+-define(COMMAND_SUBTRACT, <<"operator_subtract">>).
+-define(COMMAND_MULTIPLY, <<"operator_multiply">>).
+-define(COMMAND_DIVIDE, <<"operator_divide">>).
+-define(COMMAND_MODULO, <<"operator_modulo">>).
+
+%% Variable control
+-define(COMMAND_SET_VARIABLE, <<"data_setvariableto">>).
+-define(COMMAND_CHANGE_VARIABLE, <<"data_changevariableby">>).
+-define(COMMAND_DATA_VARIABLE, <<"data_variable">>).
+-define(COMMAND_DATA_VARIABLE_ON_CHANGE, <<"on_data_variable_update">>).
+-define(COMMAND_UI_BLOCK_VALUE, <<"data_ui_block_value">>).
+
+%% List control
+-define(COMMAND_ADD_TO_LIST, <<"data_addtolist">>).
+-define(COMMAND_DELETE_OF_LIST, <<"data_deleteoflist">>).
+-define(COMMAND_DELETE_ALL_LIST, <<"data_deletealloflist">>).
+-define(COMMAND_INSERT_AT_LIST, <<"data_insertatlist">>).
+-define(COMMAND_REPLACE_VALUE_AT_INDEX, <<"data_replaceitemoflist">>).
+-define(COMMAND_ITEM_OF_LIST, <<"data_itemoflist">>).
+-define(COMMAND_ITEMNUM_OF_LIST, <<"data_itemnumoflist">>).
+-define(COMMAND_LENGTH_OF_LIST, <<"data_lengthoflist">>).
+-define(COMMAND_LIST_CONTAINS_ITEM, <<"data_listcontainsitem">>).
+-define(COMMAND_LIST_GET_CONTENTS, <<"data_listcontents">>).
+-define(COMMAND_SET_LIST, <<"data_setlistto">>).
+
+%% Data-control operations
+%% Introduced by compiler
+-define(FLOW_LAST_VALUE, <<"flow_last_value">>).
+-define(COMMAND_PRELOAD_GETTER, <<"op_preload_getter">>).
+-define(FLOW_ON_BLOCK_RUN, <<"op_on_block_run">>).
+-define(FLOW_JUMP_TO_POSITION, <<"jump_to_position">>).
+
+%% Debugging
+-define(COMMAND_LOG_VALUE, <<"logging_add_log">>).
+-define(COMMAND_GET_THREAD_ID, <<"flow_get_thread_id">>).
diff --git a/backend/apps/automate_bot_engine/src/program_records.hrl b/backend/apps/automate_bot_engine/src/program_records.hrl
index dac3e589..428c3239 100644
--- a/backend/apps/automate_bot_engine/src/program_records.hrl
+++ b/backend/apps/automate_bot_engine/src/program_records.hrl
@@ -1,3 +1,5 @@
+-include("../../automate_common_types/src/types.hrl").
+
-record(program_trigger, { condition :: map()
, subprogram :: [any()]
}).
@@ -7,9 +9,11 @@
, global_memory :: map() % Thread-specific values TODO: rename
, instruction_memory :: map() % Memory held for each individual instruction on the program
, program_id :: binary() % ID of the program being run
+ , thread_id :: binary() |undefined % ID of the thread being run
+ , direction :: thread_direction()
}).
--record(program_permissions, { owner_user_id :: binary()
+-record(program_permissions, { owner_user_id :: owner_id()
}).
-record(program_state, { program_id :: binary()
@@ -18,3 +22,58 @@
, triggers :: [#program_trigger{}]
, enabled=true :: boolean()
}).
+
+%% Error types
+-record(index_not_in_list, { list_name :: binary()
+ , index :: non_neg_integer()
+ , max :: non_neg_integer()
+ }).
+-record(invalid_list_index_type, { list_name :: binary()
+ , index :: any()
+ }).
+
+-record(list_not_set, { list_name :: binary()
+ }).
+
+-record(variable_not_set, { variable_name :: binary()
+ }).
+
+-record(memory_not_set, { block_id :: any()
+ }).
+
+-record(memory_item_size_exceeded, { next_size :: pos_integer()
+ , max_size :: pos_integer()
+ }).
+
+-record(unknown_operation, { }).
+
+%% Bridge errors
+-record(disconnected_bridge, { bridge_id :: binary()
+ , action :: binary()
+ }).
+-record(bridge_call_connection_not_found, { bridge_id :: binary()
+ , action :: binary()
+ }).
+-record(bridge_call_timeout, { bridge_id :: binary()
+ , action :: binary()
+ }).
+-record(bridge_call_failed, { reason :: binary() | undefined
+ , bridge_id :: binary()
+ , action :: binary()
+ }).
+-record(bridge_call_error_getting_resource, { bridge_id :: binary()
+ , action :: binary()
+ }).
+
+
+-type program_error_type() :: #index_not_in_list{} | #invalid_list_index_type{}
+ | #list_not_set{} | #variable_not_set{}
+ | #memory_not_set{}
+ | #memory_item_size_exceeded{}
+ | #unknown_operation{}
+ | #disconnected_bridge{} | #bridge_call_connection_not_found{} | #bridge_call_timeout{} | #bridge_call_failed{}
+ | #bridge_call_error_getting_resource{}.
+
+-record(program_error, { error :: program_error_type()
+ , block_id :: binary() | undefined
+ }).
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_forking_flows_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_forking_flows_tests.erl
new file mode 100644
index 00000000..7977cedb
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_forking_flows_tests.erl
@@ -0,0 +1,274 @@
+%%% Automate bot engine getters tests.
+%%% @end
+
+-module(automate_bot_engine_forking_flows_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+-include("../src/program_records.hrl").
+-include("../src/instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+
+%% Test data
+-include("single_line_program.hrl").
+
+-define(APPLICATION, automate_bot_engine).
+-define(WAIT_PER_INSTRUCTION, 100). %% Milliseconds
+%% Note, if waiting per instruction takes too much time consider adding a method
+%% which checks periodically.
+-define(UTILS, automate_bot_engine_test_utils).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _Pid} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% application:stop(?APPLICATION),
+
+ ok.
+
+
+tests(_SetupResult) ->
+ %% Operations
+ %% Lists
+ [ {"[Bot engine][Fork Operations] Simple fork (no join)", fun simple_fork_no_join/0}
+ , {"[Bot engine][Fork Operations] Simple fork with join", fun simple_fork_with_join/0}
+ , {"[Bot engine][Fork Operations] Fork and join, check thread IDs", fun fork_and_join_check_thread_ids/0}
+ , {"[Bot engine][Fork Operations] Nested fork and join, check thread IDs", fun nested_fork_and_join_check_thread_ids/0}
+ , {"[Bot engine][Fork Operations] Fork fork and join first", fun fork_and_join_first/0}
+ ].
+
+%%%% Operations
+simple_fork_no_join() ->
+ ExpectedLogs = [<<"first branch">>, <<"second branch">>], %% Note that they might be shuffled
+
+ ProgramId = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ {?COMMAND_FORK_EXECUTION, []
+ , [ [ { ?COMMAND_LOG_VALUE, [constant_val(<<"first branch">>)] } ]
+ , [ { ?COMMAND_LOG_VALUE, [constant_val(<<"second branch">>)] } ]
+ ]
+ }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, Logs} = automate_bot_engine:get_user_generated_logs(ProgramId),
+
+ [ #user_generated_log_entry{event_message=First}
+ , #user_generated_log_entry{event_message=Second}
+ ] = Logs,
+
+ io:fwrite("Logs: ~p~n", [[First, Second]]),
+ io:fwrite("Expected: ~p~n", [ExpectedLogs]),
+ ?assert(([First, Second] =:= ExpectedLogs)
+ or ([Second, First] =:= ExpectedLogs)).
+
+simple_fork_with_join() ->
+ ExpectedLogs = [<<"first branch">>, <<"second branch">>, <<"joined">>], %% Note that the first two might be shuffled
+
+ ProgramId = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_FORK_EXECUTION, []
+ , [ [ {?COMMAND_LOG_VALUE, [constant_val(<<"first branch">>)]} ]
+ , [ {?COMMAND_LOG_VALUE, [constant_val(<<"second branch">>)]} ]
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"joined">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+ timer:sleep(?WAIT_PER_INSTRUCTION * 4),
+ {ok, Logs} = automate_bot_engine:get_user_generated_logs(ProgramId),
+
+ [ #user_generated_log_entry{event_message=First}
+ , #user_generated_log_entry{event_message=Second}
+ , #user_generated_log_entry{event_message=Joined}
+ ] = Logs,
+
+ io:fwrite("Logs: ~p~n", [[First, Second]]),
+ io:fwrite("Expected: ~p~n", [ExpectedLogs]),
+ ?assert(([First, Second, Joined] =:= ExpectedLogs)
+ or ([Second, First, Joined] =:= ExpectedLogs)).
+
+fork_and_join_check_thread_ids() ->
+ ProgramId = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ , { ?COMMAND_FORK_EXECUTION, []
+ , [ [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ , [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+ timer:sleep(?WAIT_PER_INSTRUCTION * 5),
+ {ok, Logs} = automate_bot_engine:get_user_generated_logs(ProgramId),
+
+ [ #user_generated_log_entry{event_message=Main}
+ , #user_generated_log_entry{event_message=First}
+ , #user_generated_log_entry{event_message=Second}
+ , #user_generated_log_entry{event_message=Main2}
+ ] = Logs,
+
+ io:fwrite("Logs: ~p~n", [[Main, First, Second, Main2]]),
+ ?assert((Main =:= Main2)
+ and (First =/= Second)).
+
+nested_fork_and_join_check_thread_ids() ->
+ ProgramId = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ , { ?COMMAND_FORK_EXECUTION, []
+ , [ [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ , { ?COMMAND_FORK_EXECUTION, []
+ , [ [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ , [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ ]
+ , [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ , { ?COMMAND_FORK_EXECUTION, []
+ , [ [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ , [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ ]
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+ timer:sleep(?WAIT_PER_INSTRUCTION * 20),
+ {ok, Logs} = automate_bot_engine:get_user_generated_logs(ProgramId),
+
+ Messages = [ Msg || #user_generated_log_entry{event_message=Msg} <- Logs ],
+ io:fwrite("Logs: ~p~n", [Messages]),
+
+ ?assertEqual(length(Messages), 10),
+
+ %% The first and the last should be the same (before fork and after the merge).
+ First = lists:nth(1, Messages),
+ ?assertEqual(First, lists:nth(10, Messages)),
+
+ %% The second should be repeated (as it's one of the top level forks, it will be joined).
+ Second = lists:nth(2, Messages),
+ ?assertEqual(length(lists:filter(fun (X) -> X =:= Second end, Messages)), 2),
+
+ CountOcurrences = fun(E, L) ->
+ length(lists:filter(fun (Y) -> E =:= Y end, L))
+ end,
+
+ %% Same for the one-to-last (in case is not the same as the Second)
+ SecondFork = case lists:nth(9, Messages) of
+ Second -> %% If it's the same as Second, find the other duplicated one
+ {value, Result} = lists:search(fun(X) ->
+ case X of
+ First -> false ;
+ Second -> false;
+ _ ->
+ CountOcurrences(X, Messages) =:= 2
+ end
+ end, Messages),
+ Result;
+ _ ->
+ OneToLast = lists:nth(9, Messages),
+ ?assertEqual(length(lists:filter(fun (X) -> X =:= OneToLast end, Messages)), 2),
+ OneToLast
+ end,
+
+ %% The rest should only appear once
+ ?assert(lists:all(fun (X) ->
+ case X of
+ First -> true;
+ Second -> true;
+ SecondFork -> true;
+ _ ->
+ %% No duplicates
+ CountOcurrences(X, Messages) =:= 1
+ end
+ end, Messages)).
+
+fork_and_join_first() ->
+ ProgramId = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ , { ?COMMAND_FORK_EXECUTION, [ constant_val(?OP_FORK_CONTINUE_ON_FIRST) ]
+ , [ [ { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ , [ { ?COMMAND_WAIT, [ constant_val(0.1) ] }
+ , { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] } ]
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [ ?UTILS:block_val({ ?COMMAND_GET_THREAD_ID }) ] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+ timer:sleep(?WAIT_PER_INSTRUCTION * 5 + 200), %% Take into account the wait operation
+ {ok, Logs} = automate_bot_engine:get_user_generated_logs(ProgramId),
+
+ Messages = [ M || #user_generated_log_entry{event_message=M} <- Logs ],
+ [ Main, First, Main2, Waited ] = Messages,
+
+ io:fwrite("Logs: ~p~n", [[Main, First, Main2, Waited]]),
+ ?assert((Main =:= Main2)
+ and (First =/= Waited)).
+
+%%====================================================================
+%% Util functions
+%%====================================================================
+constant_val(Val) ->
+ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Val
+ }.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_operations_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_getters_tests.erl
similarity index 59%
rename from backend/apps/automate_bot_engine/test/automate_bot_engine_operations_tests.erl
rename to backend/apps/automate_bot_engine/test/automate_bot_engine_getters_tests.erl
index a7a4e6ff..dbc5dddf 100644
--- a/backend/apps/automate_bot_engine/test/automate_bot_engine_operations_tests.erl
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_getters_tests.erl
@@ -1,8 +1,7 @@
-
-%%% Automate bot engine operation tests.
+%%% Automate bot engine getters tests.
%%% @end
--module(automate_bot_engine_operations_tests).
+-module(automate_bot_engine_getters_tests).
-include_lib("eunit/include/eunit.hrl").
%% Data structures
@@ -24,6 +23,7 @@
, global_memory=#{}
, instruction_memory=#{}
, program_id=undefined
+ , thread_id=undefined
}).
%%====================================================================
@@ -42,10 +42,10 @@ session_manager_test_() ->
setup() ->
NodeName = node(),
- %% %% Use a custom node name to avoid overwriting the actual databases
- %% net_kernel:start([testing, shortnames]),
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
- %% {ok, Pid} = application:ensure_all_started(?APPLICATION),
+ {ok, Pid} = application:ensure_all_started(?APPLICATION),
{NodeName}.
@@ -58,89 +58,106 @@ stop({_NodeName}) ->
tests(_SetupResult) ->
- %%# Operations
+ %% Operations
%% Join
- [ {"[Bot operation][Join] Join two str", fun join_str_and_str/0}
- , {"[Bot operation][Join] Join str to int", fun join_str_to_int/0}
- , {"[Bot operation][Join] Join str to float", fun join_str_to_float/0}
- , {"[Bot operation][Join] Join int to int", fun join_int_to_int/0}
- , {"[Bot operation][Join] Join int to float", fun join_int_to_float/0}
- , {"[Bot operation][Join] Join float to float", fun join_float_to_float/0}
+ [ {"[Bot engine][Getter][Join] Join two str", fun join_str_and_str/0}
+ , {"[Bot engine][Getter][Join] Join str to int", fun join_str_to_int/0}
+ , {"[Bot engine][Getter][Join] Join str to float", fun join_str_to_float/0}
+ , {"[Bot engine][Getter][Join] Join int to int", fun join_int_to_int/0}
+ , {"[Bot engine][Getter][Join] Join int to float", fun join_int_to_float/0}
+ , {"[Bot engine][Getter][Join] Join float to float", fun join_float_to_float/0}
%% Add
- , {"[Bot operation][Add] Add two str", fun add_str_and_str/0}
- , {"[Bot operation][Add] Add str to int", fun add_str_and_int/0}
- , {"[Bot operation][Add] Add str to float", fun add_str_and_float/0}
- , {"[Bot operation][Add] Add int to int", fun add_int_and_int/0}
- , {"[Bot operation][Add] Add int to float", fun add_int_and_float/0}
- , {"[Bot operation][Add] Add float to float", fun add_float_and_float/0}
+ , {"[Bot engine][Getter][Add] Add two str", fun add_str_and_str/0}
+ , {"[Bot engine][Getter][Add] Add str to int", fun add_str_and_int/0}
+ , {"[Bot engine][Getter][Add] Add str to float", fun add_str_and_float/0}
+ , {"[Bot engine][Getter][Add] Add int to int", fun add_int_and_int/0}
+ , {"[Bot engine][Getter][Add] Add int to float", fun add_int_and_float/0}
+ , {"[Bot engine][Getter][Add] Add float to float", fun add_float_and_float/0}
%% Subtract
- , {"[Bot operation][Sub] Subtract two str", fun sub_str_and_str/0}
- , {"[Bot operation][Sub] Subtract str from int", fun sub_str_and_int/0}
- , {"[Bot operation][Sub] Subtract str from float", fun sub_str_and_float/0}
- , {"[Bot operation][Sub] Subtract int from int", fun sub_int_and_int/0}
- , {"[Bot operation][Sub] Subtract int from float", fun sub_int_and_float/0}
- , {"[Bot operation][Sub] Subtract float from float", fun sub_float_and_float/0}
+ , {"[Bot engine][Getter][Sub] Subtract two str", fun sub_str_and_str/0}
+ , {"[Bot engine][Getter][Sub] Subtract str from int", fun sub_str_and_int/0}
+ , {"[Bot engine][Getter][Sub] Subtract str from float", fun sub_str_and_float/0}
+ , {"[Bot engine][Getter][Sub] Subtract int from int", fun sub_int_and_int/0}
+ , {"[Bot engine][Getter][Sub] Subtract int from float", fun sub_int_and_float/0}
+ , {"[Bot engine][Getter][Sub] Subtract float from float", fun sub_float_and_float/0}
%% Multiply
- , {"[Bot operation][Multiply] Multiply two str", fun mult_str_and_str/0}
- , {"[Bot operation][Multiply] Multiply str and int", fun mult_str_and_int/0}
- , {"[Bot operation][Multiply] Multiply str and float", fun mult_str_and_float/0}
- , {"[Bot operation][Multiply] Multiply int and int", fun mult_int_and_int/0}
- , {"[Bot operation][Multiply] Multiply int and float", fun mult_int_and_float/0}
- , {"[Bot operation][Multiply] Multiply float and float", fun mult_float_and_float/0}
+ , {"[Bot engine][Getter][Multiply] Multiply two str", fun mult_str_and_str/0}
+ , {"[Bot engine][Getter][Multiply] Multiply str and int", fun mult_str_and_int/0}
+ , {"[Bot engine][Getter][Multiply] Multiply str and float", fun mult_str_and_float/0}
+ , {"[Bot engine][Getter][Multiply] Multiply int and int", fun mult_int_and_int/0}
+ , {"[Bot engine][Getter][Multiply] Multiply int and float", fun mult_int_and_float/0}
+ , {"[Bot engine][Getter][Multiply] Multiply float and float", fun mult_float_and_float/0}
%% Divide
- , {"[Bot operation][Divide] Divide two str", fun divide_str_and_str/0}
- , {"[Bot operation][Divide] Divide str and int", fun divide_str_and_int/0}
- , {"[Bot operation][Divide] Divide str and float", fun divide_str_and_float/0}
- , {"[Bot operation][Divide] Divide int and int", fun divide_int_and_int/0}
- , {"[Bot operation][Divide] Divide int and float", fun divide_int_and_float/0}
- , {"[Bot operation][Divide] Divide float and float", fun divide_float_and_float/0}
+ , {"[Bot engine][Getter][Divide] Divide two str", fun divide_str_and_str/0}
+ , {"[Bot engine][Getter][Divide] Divide str and int", fun divide_str_and_int/0}
+ , {"[Bot engine][Getter][Divide] Divide str and float", fun divide_str_and_float/0}
+ , {"[Bot engine][Getter][Divide] Divide int and int", fun divide_int_and_int/0}
+ , {"[Bot engine][Getter][Divide] Divide int and float", fun divide_int_and_float/0}
+ , {"[Bot engine][Getter][Divide] Divide float and float", fun divide_float_and_float/0}
+
+ %% Modulo
+ , {"[Bot engine][Getter][Divide] Modulo", fun modulo_simple/0}
+ , {"[Bot engine][Getter][Divide] Modulo of positive and negative", fun modulo_pos_and_neg/0}
+ , {"[Bot engine][Getter][Divide] Modulo of negative and positive", fun modulo_neg_and_pos/0}
+ , {"[Bot engine][Getter][Divide] Modulo of negative and negative", fun modulo_neg_and_neg/0}
+ , {"[Bot engine][Getter][Divide] Modulo two str", fun modulo_str_and_str/0}
+ , {"[Bot engine][Getter][Divide] Modulo str and int", fun modulo_str_and_int/0}
+ , {"[Bot engine][Getter][Divide] Modulo str and float", fun modulo_str_and_float/0}
+ , {"[Bot engine][Getter][Divide] Modulo int and int", fun modulo_int_and_int/0}
+ , {"[Bot engine][Getter][Divide] Modulo int and float", fun modulo_int_and_float/0}
+ , {"[Bot engine][Getter][Divide] Modulo float and float", fun modulo_float_and_float/0}
%%# Comparisons
%% Less than
- , {"[Bot operation][Equals] Less than string and string (true)", fun lt_string_and_string_true/0}
- , {"[Bot operation][Equals] Less than string and string (false)", fun lt_string_and_string_false/0}
- , {"[Bot operation][Equals] Less than string and int (true)", fun lt_string_and_int_true/0}
- , {"[Bot operation][Equals] Less than string and int (false)", fun lt_string_and_int_false/0}
- , {"[Bot operation][Equals] Less than string and float (true)", fun lt_string_and_float_true/0}
- , {"[Bot operation][Equals] Less than string and float (false)", fun lt_string_and_float_false/0}
- , {"[Bot operation][Equals] Less than int and int (true)", fun lt_int_and_int_true/0}
- , {"[Bot operation][Equals] Less than int and int (false)", fun lt_int_and_int_false/0}
- , {"[Bot operation][Equals] Less than int and float (true)", fun lt_int_and_float_true/0}
- , {"[Bot operation][Equals] Less than int and float (false)", fun lt_int_and_float_false/0}
- , {"[Bot operation][Equals] Less than float and float (true)", fun lt_float_and_float_true/0}
- , {"[Bot operation][Equals] Less than float and float (false)", fun lt_float_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Less than string and string (true)", fun lt_string_and_string_true/0}
+ , {"[Bot engine][Getter][Equals] Less than string and string (false)", fun lt_string_and_string_false/0}
+ , {"[Bot engine][Getter][Equals] Less than string and int (true)", fun lt_string_and_int_true/0}
+ , {"[Bot engine][Getter][Equals] Less than string and int (false)", fun lt_string_and_int_false/0}
+ , {"[Bot engine][Getter][Equals] Less than string and float (true)", fun lt_string_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Less than string and float (false)", fun lt_string_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Less than int and int (true)", fun lt_int_and_int_true/0}
+ , {"[Bot engine][Getter][Equals] Less than int and int (false)", fun lt_int_and_int_false/0}
+ , {"[Bot engine][Getter][Equals] Less than int and float (true)", fun lt_int_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Less than int and float (false)", fun lt_int_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Less than float and float (true)", fun lt_float_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Less than float and float (false)", fun lt_float_and_float_false/0}
%% Greater than
- , {"[Bot operation][Equals] Greater than string and string (true)", fun gt_string_and_string_true/0}
- , {"[Bot operation][Equals] Greater than string and string (false)", fun gt_string_and_string_false/0}
- , {"[Bot operation][Equals] Greater than string and int (true)", fun gt_string_and_int_true/0}
- , {"[Bot operation][Equals] Greater than string and int (false)", fun gt_string_and_int_false/0}
- , {"[Bot operation][Equals] Greater than string and float (true)", fun gt_string_and_float_true/0}
- , {"[Bot operation][Equals] Greater than string and float (false)", fun gt_string_and_float_false/0}
- , {"[Bot operation][Equals] Greater than int and int (true)", fun gt_int_and_int_true/0}
- , {"[Bot operation][Equals] Greater than int and int (false)", fun gt_int_and_int_false/0}
- , {"[Bot operation][Equals] Greater than int and float (true)", fun gt_int_and_float_true/0}
- , {"[Bot operation][Equals] Greater than int and float (false)", fun gt_int_and_float_false/0}
- , {"[Bot operation][Equals] Greater than float and float (true)", fun gt_float_and_float_true/0}
- , {"[Bot operation][Equals] Greater than float and float (false)", fun gt_float_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Greater than string and string (true)", fun gt_string_and_string_true/0}
+ , {"[Bot engine][Getter][Equals] Greater than string and string (false)", fun gt_string_and_string_false/0}
+ , {"[Bot engine][Getter][Equals] Greater than string and int (true)", fun gt_string_and_int_true/0}
+ , {"[Bot engine][Getter][Equals] Greater than string and int (false)", fun gt_string_and_int_false/0}
+ , {"[Bot engine][Getter][Equals] Greater than string and float (true)", fun gt_string_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Greater than string and float (false)", fun gt_string_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Greater than int and int (true)", fun gt_int_and_int_true/0}
+ , {"[Bot engine][Getter][Equals] Greater than int and int (false)", fun gt_int_and_int_false/0}
+ , {"[Bot engine][Getter][Equals] Greater than int and float (true)", fun gt_int_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Greater than int and float (false)", fun gt_int_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Greater than float and float (true)", fun gt_float_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Greater than float and float (false)", fun gt_float_and_float_false/0}
%% Equal to
- , {"[Bot operation][Equals] Equal string and string (true)", fun eq_string_and_string_true/0}
- , {"[Bot operation][Equals] Equal string and string (false)", fun eq_string_and_string_false/0}
- , {"[Bot operation][Equals] Equal string and int (true)", fun eq_string_and_int_true/0}
- , {"[Bot operation][Equals] Equal string and int (false)", fun eq_string_and_int_false/0}
- , {"[Bot operation][Equals] Equal string and float (true)", fun eq_string_and_float_true/0}
- , {"[Bot operation][Equals] Equal string and float (false)", fun eq_string_and_float_false/0}
- , {"[Bot operation][Equals] Equal int and int (true)", fun eq_int_and_int_true/0}
- , {"[Bot operation][Equals] Equal int and int (false)", fun eq_int_and_int_false/0}
- , {"[Bot operation][Equals] Equal int and float (true)", fun eq_int_and_float_true/0}
- , {"[Bot operation][Equals] Equal int and float (false)", fun eq_int_and_float_false/0}
- , {"[Bot operation][Equals] Equal float and float (true)", fun eq_float_and_float_true/0}
- , {"[Bot operation][Equals] Equal float and float (false)", fun eq_float_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Equal string and string (true)", fun eq_string_and_string_true/0}
+ , {"[Bot engine][Getter][Equals] Equal string and string (false)", fun eq_string_and_string_false/0}
+ , {"[Bot engine][Getter][Equals] Equal string and int (true)", fun eq_string_and_int_true/0}
+ , {"[Bot engine][Getter][Equals] Equal string and int (false)", fun eq_string_and_int_false/0}
+ , {"[Bot engine][Getter][Equals] Equal string and float (true)", fun eq_string_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Equal string and float (false)", fun eq_string_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Equal int and int (true)", fun eq_int_and_int_true/0}
+ , {"[Bot engine][Getter][Equals] Equal int and int (false)", fun eq_int_and_int_false/0}
+ , {"[Bot engine][Getter][Equals] Equal int and float (true)", fun eq_int_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Equal int and float (false)", fun eq_int_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Equal float and float (true)", fun eq_float_and_float_true/0}
+ , {"[Bot engine][Getter][Equals] Equal float and float (false)", fun eq_float_and_float_false/0}
+ , {"[Bot engine][Getter][Equals] Variadic equals (false)", fun eq_variadic_false/0}
+ , {"[Bot engine][Getter][Equals] Variadic equals (true)", fun eq_variadic_true/0}
+
+ , {"[Bot engine][Getter][Preload/Last-Value] Sample int-int equality (true)", fun preload_last_val_eq_int_int_true/0}
+ , {"[Bot engine][Getter][Preload/Last-Value] Sample int-int equality (false)", fun preload_last_val_eq_int_int_false/0}
].
%%%% Operations
@@ -151,7 +168,7 @@ join_str_and_str()->
, constant_val(<<"2">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, <<"22">>}, R).
+ ?assertMatch({ok, <<"22">>, _}, R).
join_str_to_int()->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_JOIN
@@ -159,7 +176,7 @@ join_str_to_int()->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, <<"22">>}, R).
+ ?assertMatch({ok, <<"22">>, _}, R).
join_str_to_float()->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_JOIN
@@ -167,7 +184,7 @@ join_str_to_float()->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, <<"22.2">>}, R).
+ ?assertMatch({ok, <<"22.2">>, _}, R).
join_int_to_int()->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_JOIN
@@ -175,7 +192,7 @@ join_int_to_int()->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, <<"22">>}, R).
+ ?assertMatch({ok, <<"22">>, _}, R).
join_int_to_float()->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_JOIN
@@ -183,7 +200,7 @@ join_int_to_float()->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, <<"22.2">>}, R).
+ ?assertMatch({ok, <<"22.2">>, _}, R).
join_float_to_float()->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_JOIN
@@ -191,7 +208,7 @@ join_float_to_float()->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, <<"2.22.2">>}, R).
+ ?assertMatch({ok, <<"2.22.2">>, _}, R).
%%% Add
@@ -201,7 +218,7 @@ add_str_and_str() ->
, constant_val(<<"2">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4}, R).
+ ?assertMatch({ok, 4, _}, R).
add_str_and_int() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_ADD
@@ -209,7 +226,7 @@ add_str_and_int() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4}, R).
+ ?assertMatch({ok, 4, _}, R).
add_str_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_ADD
@@ -217,7 +234,7 @@ add_str_and_float() ->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4.2}, R).
+ ?assertMatch({ok, 4.2, _}, R).
add_int_and_int() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_ADD
@@ -225,7 +242,7 @@ add_int_and_int() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4}, R).
+ ?assertMatch({ok, 4, _}, R).
add_int_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_ADD
@@ -233,7 +250,7 @@ add_int_and_float() ->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4.2}, R).
+ ?assertMatch({ok, 4.2, _}, R).
add_float_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_ADD
@@ -241,7 +258,7 @@ add_float_and_float() ->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4.4}, R).
+ ?assertMatch({ok, 4.4, _}, R).
%%% Substract
sub_str_and_str() ->
@@ -250,7 +267,7 @@ sub_str_and_str() ->
, constant_val(<<"2">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 0}, R).
+ ?assertMatch({ok, 0, _}, R).
sub_str_and_int() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
@@ -258,38 +275,38 @@ sub_str_and_int() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 0}, R).
+ ?assertMatch({ok, 0, _}, R).
sub_str_and_float() ->
- {ok, R} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
- , ?ARGUMENTS => [ constant_val(<<"2">>)
- , constant_val(2.2)
- ]
- }, ?EMPTY_THREAD),
+ {ok, R, _} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
+ , ?ARGUMENTS => [ constant_val(<<"2">>)
+ , constant_val(2.2)
+ ]
+ }, ?EMPTY_THREAD),
?assert(approx(R, -0.2)).
sub_int_and_int() ->
- {ok, R} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
- , ?ARGUMENTS => [ constant_val(2)
- , constant_val(2.2)
- ]
- }, ?EMPTY_THREAD),
+ {ok, R, _} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
+ , ?ARGUMENTS => [ constant_val(2)
+ , constant_val(2.2)
+ ]
+ }, ?EMPTY_THREAD),
?assert(approx(R, -0.2)).
sub_int_and_float() ->
- {ok, R} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
- , ?ARGUMENTS => [ constant_val(2)
- , constant_val(2.2)
- ]
- }, ?EMPTY_THREAD),
+ {ok, R, _} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
+ , ?ARGUMENTS => [ constant_val(2)
+ , constant_val(2.2)
+ ]
+ }, ?EMPTY_THREAD),
?assert(approx(R, -0.2)).
sub_float_and_float() ->
- {ok, R} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
- , ?ARGUMENTS => [ constant_val(2.1)
- , constant_val(2.2)
- ]
- }, ?EMPTY_THREAD),
+ {ok, R, _} = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_SUBTRACT
+ , ?ARGUMENTS => [ constant_val(2.1)
+ , constant_val(2.2)
+ ]
+ }, ?EMPTY_THREAD),
?assert(approx(R, -0.1)).
%%% Multiply
@@ -299,7 +316,7 @@ mult_str_and_str() ->
, constant_val(<<"2">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4}, R).
+ ?assertMatch({ok, 4, _}, R).
mult_str_and_int() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MULTIPLY
@@ -307,7 +324,7 @@ mult_str_and_int() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4}, R).
+ ?assertMatch({ok, 4, _}, R).
mult_str_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MULTIPLY
@@ -315,7 +332,7 @@ mult_str_and_float() ->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4.4}, R).
+ ?assertMatch({ok, 4.4, _}, R).
mult_int_and_int() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MULTIPLY
@@ -323,7 +340,7 @@ mult_int_and_int() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4}, R).
+ ?assertMatch({ok, 4, _}, R).
mult_int_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MULTIPLY
@@ -331,7 +348,7 @@ mult_int_and_float() ->
, constant_val(2.2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4.4}, R).
+ ?assertMatch({ok, 4.4, _}, R).
mult_float_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MULTIPLY
@@ -339,7 +356,7 @@ mult_float_and_float() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 2.25}, R).
+ ?assertMatch({ok, 2.25, _}, R).
%%% Divide
divide_str_and_str() ->
@@ -348,7 +365,7 @@ divide_str_and_str() ->
, constant_val(<<"2">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 5.0}, R).
+ ?assertMatch({ok, 5.0, _}, R).
divide_str_and_int() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_DIVIDE
@@ -356,7 +373,7 @@ divide_str_and_int() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 5.0}, R).
+ ?assertMatch({ok, 5.0, _}, R).
divide_str_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_DIVIDE
@@ -364,7 +381,7 @@ divide_str_and_float() ->
, constant_val(2.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4.0}, R).
+ ?assertMatch({ok, 4.0, _}, R).
divide_int_and_int() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_DIVIDE
@@ -372,7 +389,7 @@ divide_int_and_int() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 5.0}, R).
+ ?assertMatch({ok, 5.0, _}, R).
divide_int_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_DIVIDE
@@ -380,7 +397,7 @@ divide_int_and_float() ->
, constant_val(2.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 4.0}, R).
+ ?assertMatch({ok, 4.0, _}, R).
divide_float_and_float() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_DIVIDE
@@ -388,7 +405,91 @@ divide_float_and_float() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, 5.25}, R).
+ ?assertMatch({ok, 5.25, _}, R).
+
+%%% Modulo
+modulo_simple() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(4)
+ , constant_val(2)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 0.0, _}, R).
+
+%% Difference between remainder and modulo
+modulo_pos_and_neg() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(7)
+ , constant_val(-3)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, -2.0, _}, R).
+
+%% Difference between remainder and modulo
+modulo_neg_and_pos() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(-7)
+ , constant_val(3)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 2.0, _}, R).
+
+%% Difference between remainder and modulo
+modulo_neg_and_neg() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(-7)
+ , constant_val(-3)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, -1.0, _}, R).
+
+modulo_str_and_str() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(<<"5">>)
+ , constant_val(<<"2">>)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 1.0, _}, R).
+
+modulo_str_and_int() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(<<"5">>)
+ , constant_val(2)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 1.0, _}, R).
+
+modulo_str_and_float() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(<<"5">>)
+ , constant_val(2.0)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 1.0, _}, R).
+
+modulo_int_and_int() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(5)
+ , constant_val(2)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 1.0, _}, R).
+
+modulo_int_and_float() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(5)
+ , constant_val(2.0)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 1.0, _}, R).
+
+modulo_float_and_float() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_MODULO
+ , ?ARGUMENTS => [ constant_val(5.25)
+ , constant_val(2)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, 1.25, _}, R).
%%%% Comparisons
%%% Less than
@@ -398,7 +499,7 @@ lt_string_and_string_true() ->
, constant_val(<<"2">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
lt_string_and_string_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -406,7 +507,7 @@ lt_string_and_string_false() ->
, constant_val(<<"0">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
lt_string_and_int_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -414,7 +515,7 @@ lt_string_and_int_true() ->
, constant_val(1)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
lt_string_and_int_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -422,7 +523,7 @@ lt_string_and_int_false() ->
, constant_val(1)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
lt_string_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -430,7 +531,7 @@ lt_string_and_float_true() ->
, constant_val(2.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
lt_string_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -438,7 +539,7 @@ lt_string_and_float_false() ->
, constant_val(1.1)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
lt_int_and_int_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -446,7 +547,7 @@ lt_int_and_int_true() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
lt_int_and_int_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -454,7 +555,7 @@ lt_int_and_int_false() ->
, constant_val(0)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
lt_int_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -462,7 +563,7 @@ lt_int_and_float_true() ->
, constant_val(0.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
lt_int_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -470,7 +571,7 @@ lt_int_and_float_false() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
lt_float_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -478,7 +579,7 @@ lt_float_and_float_true() ->
, constant_val(2.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
lt_float_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_LESS_THAN
@@ -486,7 +587,7 @@ lt_float_and_float_false() ->
, constant_val(0.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
%%% Greater than
gt_string_and_string_true() ->
@@ -495,7 +596,7 @@ gt_string_and_string_true() ->
, constant_val(<<"1">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
gt_string_and_string_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -503,7 +604,7 @@ gt_string_and_string_false() ->
, constant_val(<<"1">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
gt_string_and_int_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -511,7 +612,7 @@ gt_string_and_int_true() ->
, constant_val(0)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
gt_string_and_int_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -519,7 +620,7 @@ gt_string_and_int_false() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
gt_string_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -527,7 +628,7 @@ gt_string_and_float_true() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
gt_string_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -535,7 +636,7 @@ gt_string_and_float_false() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
gt_int_and_int_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -543,7 +644,7 @@ gt_int_and_int_true() ->
, constant_val(1)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
gt_int_and_int_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -551,7 +652,7 @@ gt_int_and_int_false() ->
, constant_val(1)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
gt_int_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -559,7 +660,7 @@ gt_int_and_float_true() ->
, constant_val(0.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
gt_int_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -567,7 +668,7 @@ gt_int_and_float_false() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
gt_float_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -575,7 +676,7 @@ gt_float_and_float_true() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
gt_float_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_GREATER_THAN
@@ -583,7 +684,7 @@ gt_float_and_float_false() ->
, constant_val(2.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
%%% Equal to
eq_string_and_string_true() ->
@@ -592,7 +693,7 @@ eq_string_and_string_true() ->
, constant_val(<<"1">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
eq_string_and_string_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -600,7 +701,7 @@ eq_string_and_string_false() ->
, constant_val(<<"2">>)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
eq_string_and_int_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -608,7 +709,7 @@ eq_string_and_int_true() ->
, constant_val(1)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
eq_string_and_int_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -616,7 +717,7 @@ eq_string_and_int_false() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
eq_string_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -624,7 +725,7 @@ eq_string_and_float_true() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
eq_string_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -632,7 +733,7 @@ eq_string_and_float_false() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
eq_int_and_int_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -640,7 +741,7 @@ eq_int_and_int_true() ->
, constant_val(1)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
eq_int_and_int_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -648,7 +749,7 @@ eq_int_and_int_false() ->
, constant_val(2)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
eq_int_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -656,7 +757,7 @@ eq_int_and_float_true() ->
, constant_val(1.0)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
eq_int_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -664,7 +765,7 @@ eq_int_and_float_false() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
eq_float_and_float_true() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -672,7 +773,7 @@ eq_float_and_float_true() ->
, constant_val(1.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, true}, R).
+ ?assertMatch({ok, true, _}, R).
eq_float_and_float_false() ->
R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
@@ -680,7 +781,69 @@ eq_float_and_float_false() ->
, constant_val(2.5)
]
}, ?EMPTY_THREAD),
- ?assertMatch({ok, false}, R).
+ ?assertMatch({ok, false, _}, R).
+
+eq_variadic_false() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
+ , ?ARGUMENTS => [ constant_val(1)
+ , constant_val(1.0)
+ , constant_val(<<"99999999.0">>)
+ , constant_val(<<"1">>)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, false, _}, R).
+
+eq_variadic_true() ->
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?COMMAND_EQUALS
+ , ?ARGUMENTS => [ constant_val(1)
+ , constant_val(1.0)
+ , constant_val(<<"1.0">>)
+ , constant_val(<<"1">>)
+ ]
+ }, ?EMPTY_THREAD),
+ ?assertMatch({ok, true, _}, R).
+
+preload_last_val_eq_int_int_true() ->
+ {ran_this_tick, Thread, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_PRELOAD_GETTER
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_BLOCK
+ , ?VALUE => [ #{ ?TYPE => ?COMMAND_EQUALS
+ , ?ARGUMENTS => [ constant_val(1)
+ , constant_val(1)
+ ]
+ , ?BLOCK_ID => <<"reference">>
+ }
+ ]
+ }
+ ]
+ }, ?EMPTY_THREAD, {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?FLOW_LAST_VALUE
+ , ?ARGUMENTS => [ constant_val(<<"reference">>)
+ , constant_val(1)
+ ]
+ }, Thread),
+ ?assertMatch({ok, true, _}, R).
+
+preload_last_val_eq_int_int_false() ->
+ {ran_this_tick, Thread, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_PRELOAD_GETTER
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_BLOCK
+ , ?VALUE => [ #{ ?TYPE => ?COMMAND_EQUALS
+ , ?ARGUMENTS => [ constant_val(0)
+ , constant_val(1)
+ ]
+ , ?BLOCK_ID => <<"reference">>
+ }
+ ]
+ }
+ ]
+ }, ?EMPTY_THREAD, {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(#{ ?TYPE => ?FLOW_LAST_VALUE
+ , ?ARGUMENTS => [ constant_val(<<"reference">>)
+ , constant_val(1)
+ ]
+ }, Thread),
+ ?assertMatch({ok, false, _}, R).
%%====================================================================
%% Util functions
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_limits_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_limits_tests.erl
new file mode 100644
index 00000000..1ab63d1d
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_limits_tests.erl
@@ -0,0 +1,137 @@
+%%% Automate bot engine limits tests.
+%%% @end
+
+-module(automate_bot_engine_limits_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+-include("../src/program_records.hrl").
+-include("../src/instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+
+%% Test data
+-include("single_line_program.hrl").
+-include("../../automate_common_types/src/limits.hrl").
+
+-define(APPLICATION, automate_bot_engine).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, Pid} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% application:stop(?APPLICATION),
+
+ ok.
+
+
+tests(_SetupResult) ->
+ %% Operations
+ %% Lists
+ [ {"[Bot engine][Limits] Limit in string concatenation", fun limit_string_concatenation/0}
+ , {"[Bot engine][Limits] Limit in list building", fun limit_add_to_list/0}
+ ].
+
+%%%% Operations
+limit_string_concatenation() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test">>, <<"">>, undefined),
+
+ %% Builda string just one block off from tripping the limit
+ Block = <<"01234567">>,
+ Iters = ceil((?USER_PROGRAM_MAX_VAR_SIZE + 1) / size(Block)) - 1,
+ Chunk = binary:list_to_bin(lists:foldl(fun(_, Acc) -> [ Block | Acc ] end, [], lists:seq(1, Iters))),
+
+ %% Considering that the limit is 1M, we should be able to add 256×2048byte strings
+ ?assertException(throw, {program_error, {memory_item_size_exceeded, _, ?USER_PROGRAM_MAX_VAR_SIZE } , _ },
+ automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_SET_VARIABLE
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_VARIABLE
+ , ?VALUE => <<"test">>
+ }
+ , #{ ?TYPE => ?VARIABLE_BLOCK
+ , ?VALUE => [ #{ ?TYPE => ?COMMAND_JOIN
+ , ?ARGUMENTS => [ constant_val(Chunk)
+ , constant_val(Block)
+ ]
+ }
+ ]
+ }
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, test})).
+
+limit_add_to_list() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test">>, [], undefined),
+
+ %% Considering that the limit is 1M, we should be able to add 64×8192byte strings
+ Block = <<"0123456789ABCDEF">>,
+ Chunk = binary:list_to_bin(lists:foldl(fun(_, Acc) -> [ Block | Acc ] end, [], lists:seq(1, 512))),
+
+ Thread2 = lists:foldl(fun(_, InnerThread) ->
+ {ran_this_tick, Thread2, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_ADD_TO_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test">>
+ }
+ , constant_val(Chunk)
+ ]
+ }, InnerThread, {?SIGNAL_PROGRAM_TICK, test}),
+
+ Thread2
+ end, Thread, lists:seq(1, 64)),
+
+ %% But adding 64 + 1 more should not be possible
+ ?assertException(throw, {program_error, {memory_item_size_exceeded, _, ?USER_PROGRAM_MAX_VAR_SIZE } , _ },
+ lists:foldl(fun(_, InnerThread) ->
+ {ran_this_tick, Thread3, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_ADD_TO_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test">>
+ }
+ , constant_val(Chunk)
+ ]
+ }, InnerThread, {?SIGNAL_PROGRAM_TICK, test}),
+
+ Thread3
+ end, Thread2, lists:seq(1, 64 + 1))).
+
+%%====================================================================
+%% Util functions
+%%====================================================================
+constant_val(Val) ->
+ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Val
+ }.
+
+empty_thread() ->
+ {_, _, ProgramId} = automate_bot_engine_test_utils:create_anonymous_program(),
+ #program_thread{ position = [1]
+ , program=[undefined]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ }.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_link_threads_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_link_threads_tests.erl
index eb280662..5b11c38d 100644
--- a/backend/apps/automate_bot_engine/test/automate_bot_engine_link_threads_tests.erl
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_link_threads_tests.erl
@@ -11,13 +11,11 @@
-include("../src/instructions.hrl").
-include("../../automate_channel_engine/src/records.hrl").
-%% Test data
--include("just_wait_program.hrl").
-
-define(APPLICATION, automate_bot_engine).
-define(TEST_NODES, [node()]).
-define(TEST_SERVICE, automate_service_registry_test_service:get_uuid()).
-define(TEST_SERVICE_ACTION, test_action).
+-define(UTILS, automate_bot_engine_test_utils).
%%====================================================================
%% Test API
@@ -45,7 +43,7 @@ setup() ->
%% @doc App infrastructure teardown.
%% @end
stop({_NodeName}) ->
- application:stop(?APPLICATION),
+ %% application:stop(?APPLICATION),
ok.
@@ -72,20 +70,22 @@ thread_link_utc_seconds() ->
thread_link(OrigCall, ResultAction, ServiceId) ->
%% Program creation
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
{Username, ProgramName, ProgramId} = create_anonymous_program(),
%% Launch program
- Blocks = [[ ?JUST_WAIT_PROGRAM_TRIGGER
- | wait_and_print(OrigCall)]],
+ Blocks = [[ ?UTILS:monitor_program_trigger(ChannelId)
+ | wait_and_print(OrigCall)]],
?assertMatch({ok, ProgramId},
automate_storage:update_program(
Username, ProgramName,
- #stored_program_content{ type=?JUST_WAIT_PROGRAM_TYPE
+ #stored_program_content{ type= <<"scratch_program">>
, parsed=#{ <<"blocks">> => Blocks
- , <<"variables">> => ?JUST_WAIT_PROGRAM_VARIABLES
+ , <<"variables">> => []
}
- , orig=?JUST_WAIT_PROGRAM_ORIG
+ , orig= <<"">>
+ , pages=#{}
})),
?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
@@ -97,7 +97,7 @@ thread_link(OrigCall, ResultAction, ServiceId) ->
?assert(is_process_alive(ProgramPid)),
%% Trigger sent, thread is spawned
- ProgramPid ! {channel_engine, ?JUST_WAIT_MONITOR_ID, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
+ ProgramPid ! {channel_engine, ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
ok = wait_for_check_ok(
fun() ->
case automate_storage:get_threads_from_program(ProgramId) of
@@ -120,14 +120,17 @@ thread_link(OrigCall, ResultAction, ServiceId) ->
%% Get second block, first argument, <<"type">>
?assertMatch(#{ ?ARGUMENTS :=
- [ #{ ?TYPE := ?COMMAND_CALL_SERVICE
- , ?ARGUMENTS :=
- #{ ?SERVICE_ACTION := ResultAction
- , ?SERVICE_ID := ServiceId
- , ?SERVICE_CALL_VALUES :=
- #{ ?TYPE := OrigCall
- }
- }
+ [ #{ ?TYPE := ?VARIABLE_BLOCK
+ , ?VALUE := [ #{ ?TYPE := ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS :=
+ #{ ?SERVICE_ACTION := ResultAction
+ , ?SERVICE_ID := ServiceId
+ , ?SERVICE_CALL_VALUES :=
+ #{ ?TYPE := OrigCall
+ }
+ }
+ }
+ ]
}]},
lists:nth(2, Program)).
@@ -137,7 +140,7 @@ thread_link(OrigCall, ResultAction, ServiceId) ->
%%====================================================================
create_anonymous_program() ->
- {Username, UserId} = create_random_user(),
+ {Username, _UserId} = create_random_user(),
ProgramName = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
{ok, ProgramId} = automate_storage:create_program(Username, ProgramName),
@@ -180,10 +183,12 @@ wait_and_print(X) -> [#{<<"args">> => [#{<<"type">> => <<"constant">>,
, <<"contents">> => []
, <<"type">> => <<"control_wait">>
},
- #{<<"args">> => [#{<<"type">> => X
- , <<"args">> => []
- , <<"contents">> => []
- }]
+ #{<<"args">> => [#{ ?TYPE => ?VARIABLE_BLOCK
+ , ?VALUE => [ #{ <<"type">> => X
+ }
+ ]
+ }
+ ]
, <<"contents">> => []
- , <<"type">> => <<"control_print">>
+ , <<"type">> => ?COMMAND_LOG_VALUE
}].
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_list_operations_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_list_operations_tests.erl
new file mode 100644
index 00000000..2983cd49
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_list_operations_tests.erl
@@ -0,0 +1,421 @@
+%%% Automate bot engine getters tests.
+%%% @end
+
+-module(automate_bot_engine_list_operations_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+-include("../src/program_records.hrl").
+-include("../src/instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+
+%% Test data
+-include("single_line_program.hrl").
+
+-define(APPLICATION, automate_bot_engine).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, Pid} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% application:stop(?APPLICATION),
+
+ ok.
+
+
+tests(_SetupResult) ->
+ %% Operations
+ %% Lists
+ [ {"[Bot engine][List Operations] Add to list (had values)", fun add_to_list_with_values/0}
+ , {"[Bot engine][List Operations] Add to list (not defined)", fun add_to_list_not_defined/0}
+ , {"[Bot engine][List Operations] Delete of list", fun delete_of_list/0}
+ , {"[Bot engine][List Operations] Delete all list (had values)", fun delete_all_list_with_values/0}
+ , {"[Bot engine][List Operations] Delete all list (not defined)", fun delete_all_list_not_defined/0}
+ , {"[Bot engine][List Operations] Insert at position in list (had values)", fun insert_at_position_in_list_with_values/0}
+ , {"[Bot engine][List Operations] Insert at position in list (not defined)", fun insert_at_position_in_list_not_defined/0}
+ , {"[Bot engine][List Operations] Replace item of list (had values)", fun replace_item_of_list_with_values/0}
+ , {"[Bot engine][List Operations] Replace item of list (not defined)", fun replace_item_of_list_not_defined/0}
+ , {"[Bot engine][List Operations] Get item of list (has values, found)", fun get_item_of_list_with_values_found/0}
+ , {"[Bot engine][List Operations] Get item of list (has values, not found)", fun get_item_of_list_with_values_not_found/0}
+ , {"[Bot engine][List Operations] Get item of list (not defined)", fun get_item_of_list_not_defined/0}
+ , {"[Bot engine][List Operations] Get item index of list (has values, found)", fun get_item_index_of_list_with_values_found/0}
+ , {"[Bot engine][List Operations] Get item index of list (has values, not found)", fun get_item_index_of_list_with_values_not_found/0}
+ , {"[Bot engine][List Operations] Get item index of list (not defined)", fun get_item_index_of_list_not_defined/0}
+ , {"[Bot engine][List Operations] Get length of list (has values)", fun get_length_of_list_with_values/0}
+ , {"[Bot engine][List Operations] Get length of list (not defined)", fun get_length_of_list_not_defined/0}
+ , {"[Bot engine][List Operations] Do list contains item (with values, true)", fun list_contains_item_with_values_true/0}
+ , {"[Bot engine][List Operations] Do list contains item (with values, false)", fun list_contains_item_with_values_false/0}
+ , {"[Bot engine][List Operations] Do list contains item (not defined)", fun list_contains_item_not_defined/0}
+ ].
+
+%%%% Operations
+add_to_list_with_values() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ {ran_this_tick, Thread2, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_ADD_TO_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"e">>)
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread2),
+ ?assertMatch({ok, [a, b, c, d, <<"e">>], _}, R).
+
+add_to_list_not_defined() ->
+ {ran_this_tick, Thread, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_ADD_TO_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"test value">>)
+ ]
+ }, empty_thread(), {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread),
+ ?assertMatch({ok, [<<"test value">>], _}, R).
+
+delete_of_list() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ {ran_this_tick, Thread2, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_DELETE_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(2) %% Keep in mind that arrays are 1-indexed
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread2),
+ ?assertMatch({ok, [a, c, d], _}, R).
+
+delete_all_list_with_values() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ {ran_this_tick, Thread2, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_DELETE_ALL_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread2),
+ ?assertMatch({ok, [], _}, R).
+
+delete_all_list_not_defined() ->
+ {ran_this_tick, Thread, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_DELETE_ALL_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, empty_thread(), {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread),
+ ?assertMatch({ok, [], _}, R).
+
+insert_at_position_in_list_with_values() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ {ran_this_tick, Thread2, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_INSERT_AT_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"new">>)
+ , constant_val(2)
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread2),
+ ?assertMatch({ok, [a, <<"new">>, b, c, d], _}, R).
+
+insert_at_position_in_list_not_defined() ->
+ {ran_this_tick, Thread, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_INSERT_AT_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"new">>)
+ , constant_val(2)
+ ]
+ }, empty_thread(), {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread),
+ ?assertMatch({ok, [?LIST_FILL, <<"new">>], _}, R).
+
+
+replace_item_of_list_with_values() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ {ran_this_tick, Thread2, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_REPLACE_VALUE_AT_INDEX
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(2)
+ , constant_val(<<"new">>)
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread2),
+ ?assertMatch({ok, [a, <<"new">>, c, d], _}, R).
+
+replace_item_of_list_not_defined() ->
+ {ran_this_tick, Thread, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_REPLACE_VALUE_AT_INDEX
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(2)
+ , constant_val(<<"new">>)
+ ]
+ }, empty_thread(), {?SIGNAL_PROGRAM_TICK, test}),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_GET_CONTENTS
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread),
+ ?assertMatch({ok, [?LIST_FILL, <<"new">>], _}, R).
+
+get_item_of_list_with_values_found() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_ITEM_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(2)
+ ]
+ }, Thread),
+ ?assertMatch({ok, b, _}, R).
+
+get_item_of_list_with_values_not_found() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ ok = try automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_ITEM_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(99)
+ ]
+ }, Thread)
+ of
+ {ok, _, _} -> ?assert(false)
+ catch throw:Error:_StackTrace ->
+ ?assertMatch(#program_error{error=#index_not_in_list{list_name= <<"test list">>}},
+ Error),
+ ok
+ end.
+
+get_item_of_list_not_defined() ->
+ Thread = empty_thread(),
+ ok = try automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_ITEM_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(99)
+ ]
+ }, Thread)
+ of
+ {ok, _, _} -> ?assert(false)
+ catch throw:Error:_StackTrace ->
+ ?assertMatch(#program_error{error=#list_not_set{list_name= <<"test list">>}},
+ Error),
+ ok
+ end.
+
+get_item_index_of_list_with_values_found() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [<<"a">>, <<"b">>, <<"c">>, <<"d">>], undefined),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_ITEMNUM_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"c">>)
+ ]
+ }, Thread),
+ ?assertMatch({ok, 3, _}, R).
+
+get_item_index_of_list_with_values_not_found() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_ITEMNUM_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"zzzz">>)
+ ]
+ }, Thread),
+ ?assertMatch({error, not_found}, R).
+
+get_item_index_of_list_not_defined() ->
+ Thread = empty_thread(),
+ ok = try automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_ITEMNUM_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"zzzz">>)
+ ]
+ }, Thread)
+ of
+ {ok, _, _} -> ?assert(false)
+ catch throw:Error:_StackTrace ->
+ ?assertMatch(#program_error{error=#list_not_set{list_name= <<"test list">>}},
+ Error),
+ ok
+ end.
+
+get_length_of_list_with_values() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [a, b, c, d], undefined),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LENGTH_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread),
+ ?assertMatch({ok, 4, _}, R).
+
+get_length_of_list_not_defined() ->
+ Thread = empty_thread(),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LENGTH_OF_LIST
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ ]
+ }, Thread),
+ ?assertMatch({ok, 0, _}, R).
+
+list_contains_item_with_values_true() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [<<"a">>, <<"b">>, <<"c">>, <<"d">>], undefined),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_CONTAINS_ITEM
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"b">>)
+ ]
+ }, Thread),
+ ?assertMatch({ok, true, _}, R).
+
+list_contains_item_with_values_false() ->
+ #program_thread{ program_id=ProgramId } = Thread = empty_thread(),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"test list">>, [<<"a">>, <<"b">>, <<"c">>, <<"d">>], undefined),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_CONTAINS_ITEM
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"z">>)
+ ]
+ }, Thread),
+ ?assertMatch({ok, false, _}, R).
+
+list_contains_item_not_defined() ->
+ Thread = empty_thread(),
+ R = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_LIST_CONTAINS_ITEM
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_LIST
+ , ?VALUE => <<"test list">>
+ }
+ , constant_val(<<"z">>)
+ ]
+ }, Thread),
+ ?assertMatch({ok, false, _}, R).
+
+%%====================================================================
+%% Util functions
+%%====================================================================
+constant_val(Val) ->
+ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Val
+ }.
+
+empty_thread() ->
+ {_, _, ProgramId} = automate_bot_engine_test_utils:create_anonymous_program(),
+ #program_thread{ position = [1]
+ , program=[undefined]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ }.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_operation_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_operation_tests.erl
new file mode 100644
index 00000000..6f5b1fa0
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_operation_tests.erl
@@ -0,0 +1,190 @@
+%%% Automate bot engine getters tests.
+%%% @end
+
+-module(automate_bot_engine_operation_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+-include("../src/program_records.hrl").
+-include("../src/instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+
+%% Test data
+-include("single_line_program.hrl").
+
+-define(APPLICATION, automate_bot_engine).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% %% Use a custom node name to avoid overwriting the actual databases
+ %% net_kernel:start([testing, shortnames]),
+
+ %% {ok, Pid} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% application:stop(?APPLICATION),
+
+ ok.
+
+
+tests(_SetupResult) ->
+ %% Operations
+ [ {"[Bot engine][Misc. Operations] Log value", fun test_log_value/0}
+ , {"[Bot engine][String operations] Text contains - Simple - True", fun text_contains_simple_true/0}
+ , {"[Bot engine][String operations] Text contains - Simple - False", fun text_contains_simple_false/0}
+ , {"[Bot engine][String operations] Text contains - Case insensitive - True", fun text_contains_case_insensitive/0}
+ , {"[Bot engine][String operations] Text contains - Accent insensitive - True", fun text_contains_accent_insensitive/0}
+ , {"[Bot engine][String operations] Text contains - Several Characters Map to same", fun text_contains_several_map_to_same/0}
+ %% String concat
+ , {"[Bot engine][String operations] String concatenation simple", fun string_concatenation_simple/0}
+ , {"[Bot engine][String operations] String concatenation left_empty", fun string_concatenation_left_empty/0}
+ , {"[Bot engine][String operations] String concatenation right_empty", fun string_concatenation_right_empty/0}
+ , {"[Bot engine][String operations] String concatenation single_param", fun string_concatenation_single_param/0}
+ ].
+
+%%%% Operations
+test_log_value() ->
+ #program_thread{program_id=Pid}=Thread=empty_thread(),
+ {ran_this_tick, _Thread2, _} = automate_bot_engine_operations:run_instruction(
+ #{ ?TYPE => ?COMMAND_LOG_VALUE
+ , ?ARGUMENTS => [ constant_val(<<"test line">>)
+ ]
+ }, Thread, {?SIGNAL_PROGRAM_TICK, test}),
+ Logs = automate_bot_engine:get_user_generated_logs(Pid),
+ ?assertMatch({ok, [#user_generated_log_entry{event_message= <<"test line">>}]}, Logs).
+
+%%%% Text contains operations
+text_contains_simple_true() ->
+ Thread = empty_thread(),
+ {ok, Value, _} = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_STRING_CONTAINS
+ , ?ARGUMENTS => [ constant_val(<<"this is a test string">>)
+ , constant_val(<<"test">>)
+ ]
+ }, Thread),
+ ?assertMatch(true, Value).
+
+text_contains_simple_false() ->
+ Thread = empty_thread(),
+ {ok, Value, _} = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_STRING_CONTAINS
+ , ?ARGUMENTS => [ constant_val(<<"this is a not a pass">>)
+ , constant_val(<<"test">>)
+ ]
+ }, Thread),
+ ?assertMatch(false, Value).
+
+text_contains_case_insensitive() ->
+ Thread = empty_thread(),
+ {ok, Value, _} = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_STRING_CONTAINS
+ , ?ARGUMENTS => [ constant_val(<<"this is a TEst string">>)
+ , constant_val(<<"teST">>)
+ ]
+ }, Thread),
+ ?assertMatch(true, Value).
+
+text_contains_accent_insensitive() ->
+ Thread = empty_thread(),
+ {ok, Value, _} = automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_STRING_CONTAINS
+ , ?ARGUMENTS => [ constant_val(unicode:characters_to_binary("this an accent -åäö oaa- test string", utf8))
+ , constant_val(unicode:characters_to_binary("-aao öäå-", utf8))
+ ]
+ }, Thread),
+ ?assertMatch(true, Value).
+
+text_contains_several_map_to_same() ->
+ Unicode = constant_val(unicode:characters_to_binary("Å Å ̊A ấ ą", utf8)), % Taken from http://www.macchiato.com/unicode/nfc-faq
+ Ascii = constant_val(unicode:characters_to_binary("A A A A A", utf8)),
+ Thread = empty_thread(),
+ ?assertMatch({ok, true, _}, automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_STRING_CONTAINS
+ , ?ARGUMENTS => [ Unicode
+ , Ascii
+ ]
+ }, Thread)),
+ ?assertMatch({ok, true, _}, automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_STRING_CONTAINS
+ , ?ARGUMENTS => [ Ascii
+ , Unicode
+ ]
+ }, Thread)).
+
+string_concatenation_simple() ->
+ Left = constant_val(<<"Hello, ">>),
+ Right = constant_val(<<"World!">>),
+ Thread = empty_thread(),
+ ?assertMatch({ok, <<"Hello, World!">>, _}, automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_JOIN
+ , ?ARGUMENTS => [ Left
+ , Right
+ ]
+ }, Thread)).
+
+string_concatenation_left_empty() ->
+ Left = null,
+ Right = constant_val(<<"World!">>),
+ Thread = empty_thread(),
+ ?assertMatch({ok, <<"World!">>, _}, automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_JOIN
+ , ?ARGUMENTS => [ Left
+ , Right
+ ]
+ }, Thread)).
+
+string_concatenation_right_empty() ->
+ Left = constant_val(<<"Hello, ">>),
+ Right = null,
+ Thread = empty_thread(),
+ ?assertMatch({ok, <<"Hello, ">>, _}, automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_JOIN
+ , ?ARGUMENTS => [ Left
+ , Right
+ ]
+ }, Thread)).
+
+string_concatenation_single_param() ->
+ Left = constant_val(<<"Hello, ">>),
+ Thread = empty_thread(),
+ ?assertMatch({ok, <<"Hello, ">>, _}, automate_bot_engine_operations:get_result(
+ #{ ?TYPE => ?COMMAND_JOIN
+ , ?ARGUMENTS => [ Left
+ ]
+ }, Thread)).
+
+%%====================================================================
+%% Util functions
+%%====================================================================
+constant_val(Val) ->
+ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Val
+ }.
+
+empty_thread() ->
+ #program_thread{ position = [1]
+ , program=[undefined]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=binary:list_to_bin(uuid:to_string(uuid:uuid4()))
+ , thread_id=undefined
+ }.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_program_decoder_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_program_decoder_tests.erl
index 92c5982c..0f1b8fdb 100644
--- a/backend/apps/automate_bot_engine/test/automate_bot_engine_program_decoder_tests.erl
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_program_decoder_tests.erl
@@ -33,10 +33,10 @@ session_manager_test_() ->
setup() ->
NodeName = node(),
- %% %% Use a custom node name to avoid overwriting the actual databases
- %% net_kernel:start([testing, shortnames]),
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
- %% {ok, Pid} = application:ensure_all_started(?APPLICATION),
+ {ok, Pid} = application:ensure_all_started(?APPLICATION),
{NodeName}.
@@ -56,10 +56,11 @@ tests(_SetupResult) ->
undefined_program_dont_crash() ->
ProgramId = <<"9a3f3d55-0393-4d0b-bfe8-08a7715230f8">>,
R = automate_bot_engine_program_decoder:initialize_program(ProgramId,
- #user_program_entry
- { user_id=undefined
- , program_parsed=undefined
- , enabled=true}),
+ #user_program_entry
+ { owner=undefined
+ , program_parsed=undefined
+ , enabled=true
+ }),
?assertMatch({ ok, #program_state{ program_id=ProgramId
, variables=[]
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_program_disabled.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_program_disabled_tests.erl
similarity index 87%
rename from backend/apps/automate_bot_engine/test/automate_bot_engine_program_disabled.erl
rename to backend/apps/automate_bot_engine/test/automate_bot_engine_program_disabled_tests.erl
index 644459fc..b4f17931 100644
--- a/backend/apps/automate_bot_engine/test/automate_bot_engine_program_disabled.erl
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_program_disabled_tests.erl
@@ -2,7 +2,7 @@
%%% Automate bot engine tests.
%%% @end
--module(automate_bot_engine_program_disabled).
+-module(automate_bot_engine_program_disabled_tests).
-include_lib("eunit/include/eunit.hrl").
%% Data structures
@@ -19,6 +19,7 @@
-define(TEST_MONITOR, <<"__test_monitor__">>).
-define(TEST_SERVICE, automate_service_registry_test_service:get_uuid()).
-define(TEST_SERVICE_ACTION, test_action).
+-define(UTILS, automate_bot_engine_test_utils).
%%====================================================================
%% Test API
@@ -46,7 +47,7 @@ setup() ->
%% @doc App infrastructure teardown.
%% @end
stop({_NodeName}) ->
- application:stop(?APPLICATION),
+ %% application:stop(?APPLICATION),
ok.
@@ -77,17 +78,19 @@ start_program_launch_thread_and_disable_program_it_continues() ->
%% Program creation
{Username, ProgramName, ProgramId} = create_anonymous_program(),
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
%% Launch program
?assertMatch({ok, ProgramId},
automate_storage:update_program(
Username, ProgramName,
#stored_program_content{ type=?JUST_WAIT_PROGRAM_TYPE
- , parsed=#{ <<"blocks">> => [[ ?JUST_WAIT_PROGRAM_TRIGGER
- | ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
+ , parsed=#{ <<"blocks">> => [[ ?UTILS:monitor_program_trigger(ChannelId)
+ | ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
, <<"variables">> => ?JUST_WAIT_PROGRAM_VARIABLES
}
, orig=?JUST_WAIT_PROGRAM_ORIG
+ , pages=#{}
})),
?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
@@ -99,10 +102,10 @@ start_program_launch_thread_and_disable_program_it_continues() ->
?assert(is_process_alive(ProgramPid)),
%% Trigger sent, thread is spawned
- ProgramPid ! {channel_engine, ?JUST_WAIT_MONITOR_ID, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
+ ProgramPid ! {channel_engine, ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
ok = wait_for_check_ok(fun() ->
case automate_storage:get_threads_from_program(ProgramId) of
- {ok, [ThreadId]} ->
+ {ok, [ThreadId]} ->
case automate_storage:get_thread_from_id(ThreadId) of
{ok, #running_program_thread_entry{runner_pid=undefined}} ->
io:fwrite("UNDEF~n"),
@@ -120,7 +123,7 @@ start_program_launch_thread_and_disable_program_it_continues() ->
?assert(is_process_alive(ThreadRunnerId)),
%% Disable program
- ok = automate_bot_engine:change_program_status(Username,ProgramId,false),
+ ok = automate_bot_engine:change_program_status(ProgramId, false),
%% Check that program is alive
{ok, ProgramPid2} = automate_storage:get_program_pid(ProgramId),
@@ -147,22 +150,24 @@ start_program_and_disable_it_no_commands() ->
%% ↓ ↓ ↑ ↓
%% Program *...........+-----------+.........YES
+ {Username, ProgramName, ProgramId} = create_anonymous_program(),
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
+
%% Program creation
TriggerMonitorSignal = { ?TRIGGERED_BY_MONITOR
- , { ?JUST_WAIT_MONITOR_ID, #{ ?CHANNEL_MESSAGE_CONTENT => start }}},
-
- {Username, ProgramName, ProgramId} = create_anonymous_program(),
+ , { ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => start }}},
%% Launch program
?assertMatch({ok, ProgramId},
automate_storage:update_program(
Username, ProgramName,
#stored_program_content{ type=?JUST_WAIT_PROGRAM_TYPE
- , parsed=#{ <<"blocks">> => [[ ?JUST_WAIT_PROGRAM_TRIGGER
- | ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
+ , parsed=#{ <<"blocks">> => [[ ?UTILS:monitor_program_trigger(ChannelId)
+ | ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
, <<"variables">> => ?JUST_WAIT_PROGRAM_VARIABLES
}
, orig=?JUST_WAIT_PROGRAM_ORIG
+ , pages=#{}
})),
?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
@@ -174,14 +179,14 @@ start_program_and_disable_it_no_commands() ->
?assert(is_process_alive(ProgramPid)),
%% Disable program
- ok = automate_bot_engine:change_program_status(Username,ProgramId,false),
+ ok = automate_bot_engine:change_program_status(ProgramId, false),
%% Check that program is alive
{ok, ProgramPid2} = automate_storage:get_program_pid(ProgramId),
?assert(is_process_alive(ProgramPid2)),
%% Trigger sent, thread is spawned
- ProgramPid ! {channel_engine, ?JUST_WAIT_MONITOR_ID, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
+ ProgramPid ! {channel_engine, ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
timer:sleep(1000),
{ok, Threads} = automate_storage:get_threads_from_program(ProgramId),
@@ -192,17 +197,19 @@ start_program_and_disable_it_no_commands() ->
start_program_disable_enable_and_launch_command()->
%% Program creation
{Username, ProgramName, ProgramId} = create_anonymous_program(),
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
%% Launch program
?assertMatch({ok, ProgramId},
automate_storage:update_program(
Username, ProgramName,
#stored_program_content{ type=?JUST_WAIT_PROGRAM_TYPE
- , parsed=#{ <<"blocks">> => [[ ?JUST_WAIT_PROGRAM_TRIGGER
- | ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
+ , parsed=#{ <<"blocks">> => [[ ?UTILS:monitor_program_trigger(ChannelId)
+ | ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
, <<"variables">> => ?JUST_WAIT_PROGRAM_VARIABLES
}
, orig=?JUST_WAIT_PROGRAM_ORIG
+ , pages=#{}
})),
?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
@@ -214,19 +221,19 @@ start_program_disable_enable_and_launch_command()->
?assert(is_process_alive(ProgramPid)),
%% Disable program
- ok = automate_bot_engine:change_program_status(Username,ProgramId,false),
+ ok = automate_bot_engine:change_program_status(ProgramId, false),
%% Check that program is alive
{ok, ProgramPid2} = automate_storage:get_program_pid(ProgramId),
?assert(is_process_alive(ProgramPid2)),
- ok = automate_bot_engine:change_program_status(Username,ProgramId,true),
+ ok = automate_bot_engine:change_program_status(ProgramId, true),
%% Trigger sent, thread is spawned
- ProgramPid ! {channel_engine, ?JUST_WAIT_MONITOR_ID, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
+ ProgramPid ! {channel_engine, ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
ok = wait_for_check_ok(fun() ->
case automate_storage:get_threads_from_program(ProgramId) of
- {ok, [ThreadId]} ->
+ {ok, [ThreadId]} ->
case automate_storage:get_thread_from_id(ThreadId) of
{ok, #running_program_thread_entry{runner_pid=undefined}} ->
false;
@@ -291,4 +298,4 @@ wait_for_check_ok(Check, TestTimes, SleepTime) ->
false ->
timer:sleep(SleepTime),
wait_for_check_ok(Check, TestTimes - 1, SleepTime)
- end.
+ end.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_signal_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_signal_tests.erl
new file mode 100644
index 00000000..f17fb2d8
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_signal_tests.erl
@@ -0,0 +1,504 @@
+%%% Automate bot engine getters tests.
+%%% @end
+
+-module(automate_bot_engine_signal_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+-include("../src/program_records.hrl").
+-include("../src/instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+
+%% Test data
+-include("single_line_program.hrl").
+
+-define(APPLICATION, automate_bot_engine).
+-define(WAIT_PER_INSTRUCTION, 100). %% Milliseconds
+%% Note, if waiting per instruction takes too much time consider adding a method
+%% which checks periodically.
+-define(UTILS, automate_bot_engine_test_utils).
+-define(BRIDGE_UTILS, automate_service_port_engine_test_utils).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _} = application:ensure_all_started(?APPLICATION),
+ {ok, _} = application:ensure_all_started(automate_service_port_engine),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% ok = application:stop(automate_service_port_engine),
+ %% ok = application:stop(?APPLICATION),
+
+ ok.
+
+
+tests(_SetupResult) ->
+ %% Operations
+ %% Lists
+ [ {"[Bot engine][Signal management] Wait for signal operation", fun simple_wait_for_signal/0}
+ , {"[Bot engine][Signal management] Wait for signal, check key", fun wait_for_signal_check_key/0}
+ , {"[Bot engine][Signal management] Wait for signal, check subkey", fun wait_for_signal_check_subkey/0}
+ , {"[Bot engine][Signal management] Wait for variable operation", fun simple_wait_for_variable/0}
+ , {"[Bot engine][Signal management] Wait for monitor signal", fun wait_for_monitor_signal/0}
+ , {"[Bot engine][Signal management] Wait for monitor signal, check key", fun wait_for_monitor_signal_check_key/0}
+ ].
+
+%%%% Operations
+simple_wait_for_signal() ->
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ ServicePortName = iolist_to_binary([Prefix, "-test-1-service-port"]),
+
+ {ok, ServicePortId} = automate_service_port_engine:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+ {ok, _ConnectionId} = ?BRIDGE_UTILS:establish_connection(ServicePortId, OwnerUserId),
+
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [constant_val(<<"before">>)] }
+ , { ?COMMAND_WAIT_FOR_NEXT_VALUE,
+ [ ?UTILS:block_val({ iolist_to_binary([ "services."
+ , ServicePortId
+ , ".on_new_message"
+ ])
+ })
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ %% Launch
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+
+ %% Check logs before sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsBefore} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsBefore = [ M || #user_generated_log_entry{event_message=M} <- LogsBefore ],
+
+ io:fwrite("Logs before signal: ~p~n", [MsgsBefore]),
+ ?assertMatch([ <<"before">> ], MsgsBefore),
+
+ %% Send signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"on_new_message">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"before">>, <<"after">>], MsgsAfter),
+ ok.
+
+wait_for_signal_check_key() ->
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ ServicePortName = iolist_to_binary([Prefix, "-test-1-service-port"]),
+
+ {ok, ServicePortId} = automate_service_port_engine:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => true
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+ {ok, _ConnectionId} = ?BRIDGE_UTILS:establish_connection(ServicePortId, OwnerUserId),
+
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [constant_val(<<"before">>)] }
+ , { ?COMMAND_WAIT_FOR_NEXT_VALUE,
+ [ ?UTILS:block_val({ iolist_to_binary([ "services."
+ , ServicePortId
+ , ".on_new_message"
+ ])
+ })
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ %% Launch
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+
+ %% Check logs before sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsBefore} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsBefore = [ M || #user_generated_log_entry{event_message=M} <- LogsBefore ],
+
+ io:fwrite("Logs before signal: ~p~n", [MsgsBefore]),
+ ?assertMatch([ <<"before">> ], MsgsBefore),
+
+ %% Send different signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"another key">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after different signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsNonWaited} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsNonWaited = [ M || #user_generated_log_entry{event_message=M} <- LogsNonWaited ],
+
+ io:fwrite("Logs after non-waited signal: ~p~n", [MsgsNonWaited]),
+ ?assertMatch([ <<"before">> ], MsgsNonWaited),
+
+ %% Send correct signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"on_new_message">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"before">>, <<"after">>], MsgsAfter),
+ ok.
+
+wait_for_signal_check_subkey() ->
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ ServicePortName = iolist_to_binary([Prefix, "-test-1-service-port"]),
+
+ {ok, ServicePortId} = automate_service_port_engine:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => true
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+ {ok, _ConnectionId} = ?BRIDGE_UTILS:establish_connection(ServicePortId, OwnerUserId),
+
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [constant_val(<<"before">>)] }
+ , { ?COMMAND_WAIT_FOR_NEXT_VALUE,
+ [ ?UTILS:block_val({ iolist_to_binary([ "services."
+ , ServicePortId
+ , ".on_new_message"
+ ])
+ , #{ <<"key">> => <<"on_new_message">>
+ , <<"subkey">> => #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => <<"correct">>
+ }
+ }
+ })
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ %% Launch
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+
+ %% Check logs before sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsBefore} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsBefore = [ M || #user_generated_log_entry{event_message=M} <- LogsBefore ],
+
+ io:fwrite("Logs before signal: ~p~n", [MsgsBefore]),
+ ?assertMatch([ <<"before">> ], MsgsBefore),
+
+ %% Send different signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"on_new_message">>
+ , <<"subkey">> => <<"different">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after different signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsNonWaited} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsNonWaited = [ M || #user_generated_log_entry{event_message=M} <- LogsNonWaited ],
+
+ io:fwrite("Logs after non-waited signal: ~p~n", [MsgsNonWaited]),
+ ?assertMatch([ <<"before">> ], MsgsNonWaited),
+
+ %% Send correct signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"on_new_message">>
+ , <<"subkey">> => <<"correct">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"before">>, <<"after">>], MsgsAfter),
+ ok.
+
+simple_wait_for_variable() ->
+ {_Username, _ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [constant_val(<<"before">>)] }
+ , { ?COMMAND_WAIT_FOR_NEXT_VALUE,
+ [ #{ ?TYPE => ?VARIABLE_VARIABLE
+ , ?VALUE => <<"checkpoint">>
+ }
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"checkpoint">>, false, undefined),
+
+ %% Launch
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+
+ %% Check logs before sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsBefore} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsBefore = [ M || #user_generated_log_entry{event_message=M} <- LogsBefore ],
+
+ io:fwrite("Logs before signal: ~p~n", [MsgsBefore]),
+ ?assertMatch([ <<"before">> ], MsgsBefore),
+
+ %% Update variable
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, <<"checkpoint">>, true, undefined),
+
+ %% Check logs after sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"before">>, <<"after">>], MsgsAfter),
+ ok.
+
+wait_for_monitor_signal() ->
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ ServicePortName = iolist_to_binary([Prefix, "-test-1-service-port"]),
+
+ {ok, ServicePortId} = automate_service_port_engine:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => true
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+ {ok, _ConnectionId} = ?BRIDGE_UTILS:establish_connection(ServicePortId, OwnerUserId),
+
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [constant_val(<<"before">>)] }
+ , { ?COMMAND_WAIT_FOR_NEXT_VALUE,
+ [ ?UTILS:block_val({ ?WAIT_FOR_MONITOR
+ , #{ ?FROM_SERVICE => ServicePortId }
+ })
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ %% Launch
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+
+ %% Check logs before sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsBefore} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsBefore = [ M || #user_generated_log_entry{event_message=M} <- LogsBefore ],
+
+ io:fwrite("Logs before signal: ~p~n", [MsgsBefore]),
+ ?assertMatch([ <<"before">> ], MsgsBefore),
+
+ %% Send signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"on_new_message">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"before">>, <<"after">>], MsgsAfter),
+ ok.
+
+wait_for_monitor_signal_check_key() ->
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ ServicePortName = iolist_to_binary([Prefix, "-test-1-service-port"]),
+
+ {ok, ServicePortId} = automate_service_port_engine:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => true
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+ {ok, ConnectionId} = ?BRIDGE_UTILS:establish_connection(ServicePortId, OwnerUserId),
+
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [constant_val(<<"before">>)] }
+ , { ?COMMAND_WAIT_FOR_NEXT_VALUE,
+ [ ?UTILS:block_val({ ?WAIT_FOR_MONITOR
+ , #{ ?FROM_SERVICE => ServicePortId
+ , <<"key">> => <<"on_new_message">>
+ }
+ })
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ %% Launch
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+
+ %% Check logs before sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsBefore} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsBefore = [ M || #user_generated_log_entry{event_message=M} <- LogsBefore ],
+
+ io:fwrite("Logs before signal: ~p~n", [MsgsBefore]),
+ ?assertMatch([ <<"before">> ], MsgsBefore),
+
+ %% Send different signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"another key">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after different signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsNonWaited} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsNonWaited = [ M || #user_generated_log_entry{event_message=M} <- LogsNonWaited ],
+
+ io:fwrite("Logs after non-waited signal: ~p~n", [MsgsNonWaited]),
+ ?assertMatch([ <<"before">> ], MsgsNonWaited),
+
+ %% Send correct signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"on_new_message">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ %% Check logs after sending signal
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"before">>, <<"after">>], MsgsAfter),
+ ok.
+
+%%====================================================================
+%% Util functions
+%%====================================================================
+constant_val(Val) ->
+ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Val
+ }.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_test_utils.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_test_utils.erl
new file mode 100644
index 00000000..8f5d66c6
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_test_utils.erl
@@ -0,0 +1,97 @@
+-module(automate_bot_engine_test_utils).
+
+-export([ build_ast/1
+ , block_val/1
+ , create_anonymous_program/0
+ , create_user_program/1
+ , create_random_user/0
+ , wait_for_program_alive/3
+ , wait_for_check_ok/3
+ , monitor_program_trigger/1
+ ]).
+
+-include("../src/instructions.hrl").
+
+%%====================================================================
+%% API
+%%====================================================================
+
+build_ast(Instructions) ->
+ lists:map(fun(I) -> build_ast_instruction(I) end, Instructions).
+
+block_val(Instruction) ->
+ #{ ?TYPE => ?VARIABLE_BLOCK
+ , ?VALUE => [ build_ast_instruction(Instruction)
+ ]
+ }.
+
+build_ast_instruction(Contents) when is_list(Contents) ->
+ #{ ?CONTENTS => lists:map(fun(I) -> build_ast_instruction(I) end, Contents)
+ };
+build_ast_instruction({Name}) ->
+ #{ ?TYPE => Name
+ };
+build_ast_instruction({Name, Args}) ->
+ #{ ?TYPE => Name
+ , ?ARGUMENTS => Args
+ };
+build_ast_instruction({Name, Args, Contents}) ->
+ #{ ?TYPE => Name
+ , ?ARGUMENTS => Args
+ , ?CONTENTS => lists:map(fun(I) -> build_ast_instruction(I) end, Contents)
+ }.
+
+create_anonymous_program() ->
+ {Username, _UserId} = create_random_user(),
+
+ ProgramName = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ {ok, ProgramId} = automate_storage:create_program(Username, ProgramName),
+ {Username, ProgramName, ProgramId}.
+
+create_user_program(UserId) ->
+ ProgramName = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ {ok, ProgramId} = automate_storage:create_program(UserId, ProgramName),
+ {ok, ProgramId}.
+
+create_random_user() ->
+ Username = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Password = undefined,
+ Email = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+
+ {ok, UserId} = automate_storage:create_user(Username, Password, Email, ready),
+ {Username, UserId}.
+
+%%====================================================================
+%% Util functions
+%%====================================================================
+wait_for_program_alive(_Pid, 0, _SleepTime) ->
+ {error, timeout};
+
+wait_for_program_alive(ProgramId, TestTimes, SleepTime) ->
+ case automate_storage:get_program_pid(ProgramId) of
+ {ok, _} ->
+ ok;
+ {error, not_running} ->
+ timer:sleep(SleepTime),
+ wait_for_program_alive(ProgramId, TestTimes - 1, SleepTime)
+ end.
+
+
+wait_for_check_ok(_Check, 0, _SleepTime) ->
+ {error, timeout};
+wait_for_check_ok(Check, TestTimes, SleepTime) ->
+ case Check() of
+ true -> ok;
+ false ->
+ timer:sleep(SleepTime),
+ wait_for_check_ok(Check, TestTimes - 1, SleepTime)
+ end.
+
+monitor_program_trigger(ChannelId) ->
+ #{ ?ARGUMENTS =>
+ #{ ?MONITOR_ID => ChannelId
+ , ?MONITOR_EXPECTED_VALUE => #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => start
+ }
+ }
+ , ?TYPE => ?WAIT_FOR_MONITOR}.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_tests.erl
index 4d87e3f2..4f76a8c1 100644
--- a/backend/apps/automate_bot_engine/test/automate_bot_engine_tests.erl
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_tests.erl
@@ -16,7 +16,6 @@
-define(APPLICATION, automate_bot_engine).
-define(TEST_NODES, [node()]).
--define(TEST_MONITOR, <<"__test_monitor__">>).
-define(TEST_SERVICE, automate_service_registry_test_service:get_uuid()).
-define(TEST_SERVICE_ACTION, test_action).
@@ -36,10 +35,10 @@ session_manager_test_() ->
setup() ->
NodeName = node(),
- %% %% Use a custom node name to avoid overwriting the actual databases
- %% net_kernel:start([testing, shortnames]),
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
- %% {ok, Pid} = application:ensure_all_started(?APPLICATION),
+ {ok, Pid} = application:ensure_all_started(?APPLICATION),
{NodeName}.
@@ -75,9 +74,10 @@ single_line_program_initialization() ->
%% Signals
wait_for_channel_signal() ->
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
Program = #program_state{ triggers=[#program_trigger{ condition=#{ ?TYPE => ?WAIT_FOR_MONITOR
, ?ARGUMENTS =>
- #{ ?MONITOR_ID => ?TEST_MONITOR
+ #{ ?MONITOR_ID => ChannelId
, ?MONITOR_EXPECTED_VALUE => ?MONITOR_ANY_VALUE
}
}
@@ -90,15 +90,17 @@ wait_for_channel_signal() ->
%% Argument resolution
constant_argument_resolution() ->
Value = example,
- ?assertMatch({ok, Value}, automate_bot_engine_variables:resolve_argument(#{ ?TYPE => ?VARIABLE_CONSTANT
- , ?VALUE => Value
- }, #program_thread{})).
+ ?assertMatch({ok, Value, _}, automate_bot_engine_variables:resolve_argument(#{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Value
+ }, #program_thread{},
+ undefined)).
%% Threads
trigger_thread_with_channel_signal() ->
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
Program = #program_state{ triggers=[#program_trigger{
condition=#{ ?TYPE => ?WAIT_FOR_MONITOR
- , ?ARGUMENTS => #{ ?MONITOR_ID => ?TEST_MONITOR
+ , ?ARGUMENTS => #{ ?MONITOR_ID => ChannelId
, ?MONITOR_EXPECTED_VALUE =>
#{ ?TYPE => ?VARIABLE_CONSTANT
, ?VALUE => example
@@ -110,13 +112,14 @@ trigger_thread_with_channel_signal() ->
{ok, [Thread]} = automate_bot_engine_triggers:get_triggered_threads(Program,
{ ?TRIGGERED_BY_MONITOR
- , { ?TEST_MONITOR,
+ , { ChannelId,
#{ ?CHANNEL_MESSAGE_CONTENT => example } }}),
?assertMatch(#program_thread{ position=[1], program=[#{ ?TYPE := example }] }, Thread).
run_thread_single_tick() ->
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
WaitForMonitorInstruction = #{ ?TYPE => ?WAIT_FOR_MONITOR
- , ?ARGUMENTS => #{ ?MONITOR_ID => ?TEST_MONITOR
+ , ?ARGUMENTS => #{ ?MONITOR_ID => ChannelId
, ?MONITOR_EXPECTED_VALUE => #{ ?TYPE => ?VARIABLE_CONSTANT
, ?VALUE => example
}
@@ -135,7 +138,7 @@ run_thread_single_tick() ->
}
},
TriggerMonitorSignal = { ?TRIGGERED_BY_MONITOR
- , { ?TEST_MONITOR, #{ ?CHANNEL_MESSAGE_CONTENT => example }}},
+ , { ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => example }}},
ProgramId = create_anonymous_program(),
@@ -148,10 +151,10 @@ run_thread_single_tick() ->
%% Unexpected signal (for the thread already started), does not run
#program_thread{ position=[1], program=[CallServiceInstruction] } = Thread,
- {did_not_run, waiting} = automate_bot_engine_operations:run_thread(Thread, TriggerMonitorSignal),
+ {did_not_run, waiting} = automate_bot_engine_operations:run_thread(Thread, TriggerMonitorSignal, undefined),
%% Expected signal, does run
- {ran_this_tick, NewThreadState} = automate_bot_engine_operations:run_thread(Thread, {?SIGNAL_PROGRAM_TICK, none}),
+ {ran_this_tick, NewThreadState, _} = automate_bot_engine_operations:run_thread(Thread, {?SIGNAL_PROGRAM_TICK, none}, undefined),
?assertMatch(#program_thread{position=[]}, NewThreadState).
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_thread_stopping_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_thread_stopping_tests.erl
index e71837b5..5799006f 100644
--- a/backend/apps/automate_bot_engine/test/automate_bot_engine_thread_stopping_tests.erl
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_thread_stopping_tests.erl
@@ -15,10 +15,7 @@
-include("just_wait_program.hrl").
-define(APPLICATION, automate_bot_engine).
--define(TEST_NODES, [node()]).
--define(TEST_MONITOR, <<"__test_monitor__">>).
--define(TEST_SERVICE, automate_service_registry_test_service:get_uuid()).
--define(TEST_SERVICE_ACTION, test_action).
+-define(UTILS, automate_bot_engine_test_utils).
%%====================================================================
%% Test API
@@ -46,7 +43,7 @@ setup() ->
%% @doc App infrastructure teardown.
%% @end
stop({_NodeName}) ->
- application:stop(?APPLICATION),
+ %% application:stop(?APPLICATION),
ok.
@@ -74,31 +71,33 @@ start_thread_and_stop_threads_continues() ->
%% Thread *-- wait ........YES..........X NO
%% Program creation
- {Username, ProgramName, ProgramId} = create_anonymous_program(),
+ {Username, ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
%% Launch program
?assertMatch({ok, ProgramId},
automate_storage:update_program(
Username, ProgramName,
#stored_program_content{ type=?JUST_WAIT_PROGRAM_TYPE
- , parsed=#{ <<"blocks">> => [[ ?JUST_WAIT_PROGRAM_TRIGGER
+ , parsed=#{ <<"blocks">> => [[ ?UTILS:monitor_program_trigger(ChannelId)
| ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
, <<"variables">> => ?JUST_WAIT_PROGRAM_VARIABLES
}
, orig=?JUST_WAIT_PROGRAM_ORIG
+ , pages=#{}
})),
?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
%% Check that program id alive
- ?assertMatch(ok, wait_for_program_alive(ProgramId, 10, 100)),
+ ?assertMatch(ok, ?UTILS:wait_for_program_alive(ProgramId, 10, 100)),
{ok, ProgramPid} = automate_storage:get_program_pid(ProgramId),
?assert(is_process_alive(ProgramPid)),
%% Trigger sent, thread is spawned
- ProgramPid ! {channel_engine, ?JUST_WAIT_MONITOR_ID, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
- ok = wait_for_check_ok(
+ ProgramPid ! {channel_engine, ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => start }},
+ ok = ?UTILS:wait_for_check_ok(
fun() ->
case automate_storage:get_threads_from_program(ProgramId) of
{ok, [ThreadId]} ->
@@ -118,14 +117,14 @@ start_thread_and_stop_threads_continues() ->
?assert(is_process_alive(ThreadRunnerId)),
%% Stop threads
- ok = automate_rest_api_backend:stop_program_threads(undefined, ProgramId),
+ ok = automate_bot_engine:stop_program_threads(ProgramId),
%% Check that program is alive
{ok, ProgramPid2} = automate_storage:get_program_pid(ProgramId),
?assert(is_process_alive(ProgramPid2)),
%% Check that thread is dead
- wait_for_check_ok(
+ ?UTILS:wait_for_check_ok(
fun() ->
case automate_storage:get_threads_from_program(ProgramId) of
{ok, []} -> true;
@@ -145,81 +144,39 @@ start_program_and_stop_threads_nothing() ->
%% ↓ ↓ ↑ ↓
%% Program *...........+-----------+.........YES
+ {Username, ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
+
%% Program creation
TriggerMonitorSignal = { ?TRIGGERED_BY_MONITOR
- , { ?JUST_WAIT_MONITOR_ID, #{ ?CHANNEL_MESSAGE_CONTENT => start }}},
-
- {Username, ProgramName, ProgramId} = create_anonymous_program(),
+ , { ChannelId, #{ ?CHANNEL_MESSAGE_CONTENT => start }}},
%% Launch program
?assertMatch({ok, ProgramId},
automate_storage:update_program(
Username, ProgramName,
#stored_program_content{ type=?JUST_WAIT_PROGRAM_TYPE
- , parsed=#{ <<"blocks">> => [[ ?JUST_WAIT_PROGRAM_TRIGGER
+ , parsed=#{ <<"blocks">> => [[ ?UTILS:monitor_program_trigger(ChannelId)
| ?JUST_WAIT_PROGRAM_INSTRUCTIONS ]]
, <<"variables">> => ?JUST_WAIT_PROGRAM_VARIABLES
}
, orig=?JUST_WAIT_PROGRAM_ORIG
+ , pages=#{}
})),
?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
%% Check that program id alive
- ?assertMatch(ok, wait_for_program_alive(ProgramId, 10, 100)),
+ ?assertMatch(ok, ?UTILS:wait_for_program_alive(ProgramId, 10, 100)),
{ok, ProgramPid} = automate_storage:get_program_pid(ProgramId),
?assert(is_process_alive(ProgramPid)),
%% Stop threads
- ok = automate_rest_api_backend:stop_program_threads(undefined, ProgramId),
+ ok = automate_bot_engine:stop_program_threads(ProgramId),
%% Check that program is alive
{ok, ProgramPid2} = automate_storage:get_program_pid(ProgramId),
?assert(is_process_alive(ProgramPid2)),
ok.
-
-
-%%====================================================================
-%% Util functions
-%%====================================================================
-create_anonymous_program() ->
-
- {Username, UserId} = create_random_user(),
-
- ProgramName = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
- {ok, ProgramId} = automate_storage:create_program(Username, ProgramName),
- {Username, ProgramName, ProgramId}.
-
-
-create_random_user() ->
- Username = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
- Password = undefined,
- Email = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
-
- {ok, UserId} = automate_storage:create_user(Username, Password, Email, ready),
- {Username, UserId}.
-
-wait_for_program_alive(Pid, 0, SleepTime) ->
- {error, timeout};
-
-wait_for_program_alive(ProgramId, TestTimes, SleepTime) ->
- case automate_storage:get_program_pid(ProgramId) of
- {ok, _} ->
- ok;
- {error, not_running} ->
- timer:sleep(SleepTime),
- wait_for_program_alive(ProgramId, TestTimes - 1, SleepTime)
- end.
-
-
-wait_for_check_ok(Check, 0, SleepTime) ->
- {error, timeout};
-wait_for_check_ok(Check, TestTimes, SleepTime) ->
- case Check() of
- true -> ok;
- false ->
- timer:sleep(SleepTime),
- wait_for_check_ok(Check, TestTimes - 1, SleepTime)
- end.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_timing_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_timing_tests.erl
new file mode 100644
index 00000000..6c3242d5
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_timing_tests.erl
@@ -0,0 +1,338 @@
+%%% Automate bot engine getters tests.
+%%% @end
+
+-module(automate_bot_engine_timing_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+-include("../src/program_records.hrl").
+-include("../src/instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+-include("../../automate_services_time/src/definitions.hrl").
+
+%% Test data
+-include("single_line_program.hrl").
+
+-define(APPLICATION, automate_bot_engine).
+-define(WAIT_PER_INSTRUCTION, 100). %% Milliseconds
+%% Note, if waiting per instruction takes too much time consider adding a method
+%% which checks periodically.
+-define(UTILS, automate_bot_engine_test_utils).
+-define(BRIDGE_UTILS, automate_service_port_engine_test_utils).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% ok = application:stop(automate_service_port_engine),
+ %% ok = application:stop(?APPLICATION),
+
+ ok.
+
+
+tests(_SetupResult) ->
+
+%%%% Triggers
+ [ {"[Bot engine][Timing] Wait for time signal", fun wait_for_simple_time_signal/0}
+ , {"[Bot engine][Timing] Wait for time signal without Timezone change", fun wait_for_time_signal_on_no_timezone_change/0}
+ , {"[Bot engine][Timing] Wait for time signal on Timezone change", fun wait_for_time_signal_on_timezone_change/0}
+%%%% Service discontinuity
+ , {"[Bot engine][Timing discontinuity] Scheduled tasks are executed even with interruptions", fun scheduled_handles_interruptions/0}
+ , {"[Bot engine][Timing discontinuity] Started tasks are not immediately executed", fun scheduled_not_immediate/0}
+ ].
+
+%%%% Triggers
+wait_for_simple_time_signal() ->
+ {_Username, _ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ Thread = #program_thread{ position = [1]
+ , program=?UTILS:build_ast([ { ?COMMAND_LOG_VALUE, [constant_val(<<"before">>)] }
+ , { ?COMMAND_WAIT_FOR_NEXT_VALUE,
+ [ ?UTILS:block_val({ ?WAIT_FOR_MONITOR
+ , #{ ?FROM_SERVICE => automate_services_time:get_uuid()
+ , <<"key">> => <<"utc_time">>
+ }
+ })
+ ]
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ])
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ %% Launch
+ {ok, _ThreadId} = automate_bot_engine_thread_launcher:launch_thread(ProgramId, Thread),
+
+ %% Wait ~2 seconds, should be enough for the time signal to arrive
+ timer:sleep(3000),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"before">>, <<"after">>], MsgsAfter),
+ ok.
+
+wait_for_time_signal_on_no_timezone_change() ->
+ TestTimezone = "Europe/Madrid",
+
+ %% This time (UTC) corresponds to 8:59:58 next day on the timezone.
+ %% Two seconds later, the hour will be the tested 09:00:00 .
+ TestTime = {{2021, 03, 26}, {7, 59, 58}},
+
+ ok = automate_testing:set_corrected_time(TestTime),
+
+ ProgramData = [ { ?WAIT_FOR_MONITOR_COMMAND,
+ #{ ?MONITOR_ID => #{ ?FROM_SERVICE => ?TIME_SERVICE_UUID }
+ , ?MONITOR_EXPECTED_VALUE => #{ <<"type">> => <<"constant">>
+ , <<"value">> => <<"09:00:00">>
+ }
+ , <<"timezone">> => TestTimezone
+ }
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ],
+
+ {Username, ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ %% Launch program
+ ?assertMatch({ok, ProgramId},
+ automate_storage:update_program(
+ Username, ProgramName,
+ #stored_program_content{ type=scratch_program
+ , parsed=#{ <<"blocks">> => [ ?UTILS:build_ast(ProgramData) ]
+ , <<"variables">> => []
+ }
+ , orig= <<"*test*">>
+ , pages=#{}
+ })),
+
+ ?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
+
+ %% Wait ~3 seconds, should be enough for the time signal to arrive
+ timer:sleep(3000),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ automate_testing:unset_corrected_time(),
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"after">> ], MsgsAfter),
+ ok.
+
+wait_for_time_signal_on_timezone_change() ->
+ TestTimezone = "Europe/Madrid",
+
+ %% This time (UTC) corresponds to 01:59:58 next day on the timezone.
+ %% Two seconds later, the hour will jump to 03:00:00 .
+ TestTime = {{2021, 03, 28}, {00, 59, 59}},
+
+ ok = automate_testing:set_corrected_time(TestTime),
+
+ ProgramData = [ { ?WAIT_FOR_MONITOR_COMMAND,
+ #{ ?MONITOR_ID => #{ ?FROM_SERVICE => ?TIME_SERVICE_UUID }
+ , ?MONITOR_EXPECTED_VALUE => #{ <<"type">> => <<"constant">>
+ , <<"value">> => <<"03:00:01">>
+ }
+ , <<"timezone">> => TestTimezone
+ }
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ],
+
+ {Username, ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ %% Launch program
+ ?assertMatch({ok, ProgramId},
+ automate_storage:update_program(
+ Username, ProgramName,
+ #stored_program_content{ type=scratch_program
+ , parsed=#{ <<"blocks">> => [ ?UTILS:build_ast(ProgramData) ]
+ , <<"variables">> => []
+ }
+ , orig= <<"*test*">>
+ , pages=#{}
+ })),
+
+ ?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
+
+ {ok, LogsBefore} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsBefore = [ M || #user_generated_log_entry{event_message=M} <- LogsBefore ],
+
+ io:fwrite("Immediate logs: ~p~n", [MsgsBefore]),
+ ?assertMatch([ ], MsgsBefore),
+
+ %% Wait ~4 seconds, should be enough for the time signal to arrive
+ timer:sleep(4000),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ automate_testing:unset_corrected_time(),
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"after">> ], MsgsAfter),
+
+ %% ?assertEqual(this_should_not_work_yet, false),
+ ok.
+
+%%%% Service discontinuity
+scheduled_handles_interruptions() ->
+ %% 1. Start at a given time
+ %% 2. Wait for the program to start
+ %% 3. Jump over the expected time
+ %% 4. Expect the scheduler to be run
+
+ StartTime = {{2021, 03, 26}, {7, 00, 00}},
+ ScheduledTime = <<"07:30:00">>,
+ JumpTime = {{2021, 03, 26}, {8, 00, 00}},
+ Timezone = <<"UTC">>,
+
+ ok = automate_testing:set_corrected_time(StartTime),
+
+ ProgramData = [ { ?WAIT_FOR_MONITOR_COMMAND,
+ #{ ?MONITOR_ID => #{ ?FROM_SERVICE => ?TIME_SERVICE_UUID }
+ , ?MONITOR_EXPECTED_VALUE => #{ <<"type">> => <<"constant">>
+ , <<"value">> => ScheduledTime
+ }
+ , <<"timezone">> => Timezone
+ }
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ],
+
+ {Username, ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ %% Launch program
+ ?assertMatch({ok, ProgramId},
+ automate_storage:update_program(
+ Username, ProgramName,
+ #stored_program_content{ type=scratch_program
+ , parsed=#{ <<"blocks">> => [ ?UTILS:build_ast(ProgramData) ]
+ , <<"variables">> => []
+ }
+ , orig= <<"*test*">>
+ , pages=#{}
+ })),
+
+ ?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
+
+ %% Check that program alive and has been activated once
+ ?assertMatch(ok, wait_for_program_alive(ProgramId, 10, 100)),
+ ?assertMatch(ok, wait_for_variable_in_program(ProgramId, { internal, { time_cache, { ScheduledTime, Timezone } } }, 20, 100)),
+
+ {ok, ProgramPid} = automate_storage:get_program_pid(ProgramId),
+ ?assert(is_process_alive(ProgramPid)),
+
+ %% Jump to final time
+ ok = automate_testing:set_corrected_time(JumpTime),
+
+ %% Wait ~2 seconds, should be enough for the time signal to arrive
+ timer:sleep(2000),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ automate_testing:unset_corrected_time(),
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ <<"after">> ], MsgsAfter),
+ ok.
+
+scheduled_not_immediate() ->
+ ScheduledTime = <<"07:30:00">>,
+ StartTime = {{2021, 03, 26}, {7, 31, 00}},
+
+ ok = automate_testing:set_corrected_time(StartTime),
+
+ ProgramData = [ { ?WAIT_FOR_MONITOR_COMMAND,
+ #{ ?MONITOR_ID => #{ ?FROM_SERVICE => ?TIME_SERVICE_UUID }
+ , ?MONITOR_EXPECTED_VALUE => #{ <<"type">> => <<"constant">>
+ , <<"value">> => ScheduledTime
+ }
+ , <<"timezone">> => <<"UTC">>
+ }
+ }
+ , { ?COMMAND_LOG_VALUE, [constant_val(<<"after">>)] }
+ ],
+
+ {Username, ProgramName, ProgramId} = ?UTILS:create_anonymous_program(),
+ %% Launch program
+ ?assertMatch({ok, ProgramId},
+ automate_storage:update_program(
+ Username, ProgramName,
+ #stored_program_content{ type=scratch_program
+ , parsed=#{ <<"blocks">> => [ ?UTILS:build_ast(ProgramData) ]
+ , <<"variables">> => []
+ }
+ , orig= <<"*test*">>
+ , pages=#{}
+ })),
+
+ ?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
+
+ %% Check that program id alive
+ ?assertMatch(ok, wait_for_program_alive(ProgramId, 10, 100)),
+
+ %% Wait ~2 seconds, should be enough for the time signal to arrive
+ timer:sleep(2000),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ automate_testing:unset_corrected_time(),
+
+ io:fwrite("Logs after signal: ~p~n", [MsgsAfter]),
+ ?assertMatch([ ], MsgsAfter),
+ ok.
+
+
+%%====================================================================
+%% Util functions
+%%====================================================================
+constant_val(Val) ->
+ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Val
+ }.
+
+wait_for_program_alive(_Pid, 0, _SleepTime) ->
+ {error, timeout};
+
+wait_for_program_alive(ProgramId, TestTimes, SleepTime) ->
+ case automate_storage:get_program_pid(ProgramId) of
+ {ok, _} ->
+ ok;
+ {error, not_running} ->
+ timer:sleep(SleepTime),
+ wait_for_program_alive(ProgramId, TestTimes - 1, SleepTime)
+ end.
+
+wait_for_variable_in_program(_Pid, _Key, 0, _SleepTime) ->
+ {error, timeout};
+
+wait_for_variable_in_program(ProgramId, Key, TestTimes, SleepTime) ->
+ case automate_bot_engine_variables:get_program_variable(ProgramId, Key) of
+ {ok, _} ->
+ ok;
+ {error, not_found} ->
+ timer:sleep(SleepTime),
+ wait_for_variable_in_program(ProgramId, Key, TestTimes - 1, SleepTime)
+ end.
diff --git a/backend/apps/automate_bot_engine/test/automate_bot_engine_trigger_tests.erl b/backend/apps/automate_bot_engine/test/automate_bot_engine_trigger_tests.erl
new file mode 100644
index 00000000..27f2df87
--- /dev/null
+++ b/backend/apps/automate_bot_engine/test/automate_bot_engine_trigger_tests.erl
@@ -0,0 +1,264 @@
+%%% @doc
+%%% Automate bot thread linking.
+%%% @end
+
+-module(automate_bot_engine_trigger_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+-include("../src/program_records.hrl").
+-include("../src/instructions.hrl").
+-include("../../automate_channel_engine/src/records.hrl").
+
+%% Test data
+
+-define(APPLICATION, automate_bot_engine).
+-define(TEST_NODES, [node()]).
+-define(TEST_SERVICE, automate_service_registry_test_service:get_uuid()).
+-define(TEST_SERVICE_ACTION, test_action).
+
+-define(WAIT_PER_INSTRUCTION, 100). %% Milliseconds
+%% Note, if waiting per instruction takes too much time consider adding a method
+%% which checks periodically.
+-define(UTILS, automate_bot_engine_test_utils).
+-define(BRIDGE_UTILS, automate_service_port_engine_test_utils).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% application:stop(?APPLICATION),
+
+ ok.
+
+
+tests(_SetupResult) ->
+ [ { "[Bot engine][Trigger tests] Test save-to", fun save_to_test/0 }
+ , { "[Bot engine][Trigger tests] Trigger with time", fun trigger_with_time/0 }
+ , { "[Bot engine][Trigger tests] Trigger with Tz time", fun trigger_with_tz_time/0 }
+ ].
+
+
+
+save_to_test() ->
+ %% Create service port
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ ServicePortName = iolist_to_binary([Prefix, "-test-1-service-port"]),
+ {ok, ServicePortId} = automate_service_port_engine:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => true
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+ {ok, _} = ?BRIDGE_UTILS:establish_connection(ServicePortId, OwnerUserId),
+
+ %% Program creation
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+
+ %% Launch program
+ Blocks = [ #{ <<"type">> => iolist_to_binary([ "services."
+ , ServicePortId
+ , ".on_new_message"
+ ])
+
+ , ?ARGUMENTS => #{ <<"key">> => <<"on_new_message">>
+ , <<"subkey">> => #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => <<"correct">>
+ }
+ , <<"monitor_save_value_to">> =>
+ #{ <<"type">> => <<"variable">>
+ , <<"value">> => <<"var">>
+ }
+ }
+ , <<"save_to">> => %% This is a possible error we have to handle
+ #{ <<"index">> => 1
+ , <<"type">> => <<"argument">>
+ }
+ }
+ , #{ <<"type">> => ?COMMAND_LOG_VALUE
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_VARIABLE
+ , ?VALUE => <<"var">>
+ }
+ ]
+ }
+ ],
+
+ ?assertMatch({ok, ProgramId},
+ automate_storage:update_program_by_id(
+ ProgramId, #stored_program_content{ type= <<"scratch_program">>
+ , parsed=#{ <<"blocks">> => [ Blocks ]
+ , <<"variables">> => []
+ }
+ , orig=undefined
+ , pages=#{}
+ })),
+
+ ?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
+
+ %% Check that program id alive
+ ?assertMatch(ok, ?UTILS:wait_for_program_alive(ProgramId, 10, 100)),
+
+ %% Send signal
+ ok = automate_service_port_engine:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => <<"on_new_message">>
+ , <<"subkey">> => <<"correct">>
+ , <<"to_user">> => null
+ , <<"value">> => <<"sample value">>
+ , <<"content">> => <<"sample content">>
+ }),
+
+ timer:sleep(?WAIT_PER_INSTRUCTION * 3),
+
+ {ok, LogsWaited} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsWaited = [ M || #user_generated_log_entry{event_message=M} <- LogsWaited ],
+ io:fwrite("Logs after signal: ~p~n", [MsgsWaited]),
+ ?assertMatch([ <<"sample content">> ], MsgsWaited),
+ ok.
+
+trigger_with_time() ->
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+
+ {_, { StartHour, StartMin, StartSec }} = calendar:now_to_datetime(erlang:timestamp()),
+
+ {MegaSeconds, Seconds, MicroSeconds} = erlang:timestamp(),
+
+ {{_Year, _Month, _Day}, {Hour, Min, Sec}} = calendar:now_to_datetime({MegaSeconds, Seconds + 2, MicroSeconds}),
+ %% Wait for the next second
+ Value = binary:list_to_bin(lists:flatten(io_lib:format("~p:~p:~p", [Hour, Min, Sec]))),
+ io:fwrite("Waiting for: ~p (~s)~n", [Value, Value]),
+ Blocks = [ #{ <<"type">> => <<"wait_for_monitor">>
+ , ?ARGUMENTS => #{ ?MONITOR_EXPECTED_VALUE => #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => Value
+ }
+ , ?MONITOR_ID => #{ ?FROM_SERVICE => automate_services_time:get_uuid() }
+ }
+ }
+ , #{ <<"type">> => ?COMMAND_LOG_VALUE
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => <<"after">>
+ }
+ ]
+ }
+ ],
+
+ ?assertMatch({ok, ProgramId},
+ automate_storage:update_program_by_id(
+ ProgramId, #stored_program_content{ type= <<"scratch_program">>
+ , parsed=#{ <<"blocks">> => [ Blocks ]
+ , <<"variables">> => []
+ }
+ , orig=undefined
+ , pages=#{}
+ })),
+
+ ?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
+
+ io:fwrite("PID: ~p~n", [ProgramId]),
+
+ %% Check that program is alive
+ ?assertMatch(ok, ?UTILS:wait_for_program_alive(ProgramId, 10, 100)),
+
+ %% Wait >2 seconds, should be enough for the time signal to arrive
+ timer:sleep(4000),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ {_, {AfterHour, AfterMin, AfterSec}} = calendar:now_to_datetime(erlang:timestamp()),
+
+ io:fwrite("Logs after ~p→~p: ~p~n", [{StartHour, StartMin, StartSec}, {AfterHour, AfterMin, AfterSec}, MsgsAfter]),
+ ?assertMatch([ <<"after">>], MsgsAfter),
+ ok.
+
+trigger_with_tz_time() ->
+ Prefix = erlang:atom_to_list(?MODULE),
+ {_, UserId} = ?UTILS:create_random_user(),
+ OwnerUserId = { user, UserId },
+ {ok, ProgramId} = ?UTILS:create_user_program(OwnerUserId),
+
+ TestTimezone = <<"Etc/GMT+1">>,
+
+ {_, { StartHour, StartMin, StartSec }} = calendar:now_to_datetime(erlang:timestamp()),
+
+ {MegaSeconds, Seconds, MicroSeconds} = erlang:timestamp(),
+ {_, { Hour, Min, Sec }} = qdate:to_date(TestTimezone, calendar:now_to_datetime({MegaSeconds, Seconds + 2, MicroSeconds})),
+
+ TestTime = binary:list_to_bin(lists:flatten(io_lib:format("~p:~p:~p", [Hour, Min, Sec]))),
+
+
+ io:fwrite("Waiting for: ~p (~s ~s)~n", [TestTime, TestTime, TestTimezone]),
+ Blocks = [ #{ <<"type">> => <<"wait_for_monitor">>
+ , ?ARGUMENTS => #{ ?MONITOR_EXPECTED_VALUE => #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => TestTime
+ }
+ , ?MONITOR_ID => #{ ?FROM_SERVICE => automate_services_time:get_uuid() }
+ , <<"timezone">> => TestTimezone
+ }
+ }
+ , #{ <<"type">> => ?COMMAND_LOG_VALUE
+ , ?ARGUMENTS => [ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => <<"after">>
+ }
+ ]
+ }
+ ],
+
+ ?assertMatch({ok, ProgramId},
+ automate_storage:update_program_by_id(
+ ProgramId, #stored_program_content{ type= <<"scratch_program">>
+ , parsed=#{ <<"blocks">> => [ Blocks ]
+ , <<"variables">> => []
+ }
+ , orig=undefined
+ , pages=#{}
+ })),
+
+ ?assertMatch(ok, automate_bot_engine_launcher:update_program(ProgramId)),
+
+ io:fwrite("PID: ~p~n", [ProgramId]),
+
+ %% Check that program is alive
+ ?assertMatch(ok, ?UTILS:wait_for_program_alive(ProgramId, 10, 100)),
+
+ %% Wait >2 seconds, should be enough for the time signal to arrive
+ timer:sleep(4000),
+ {ok, LogsAfter} = automate_bot_engine:get_user_generated_logs(ProgramId),
+ MsgsAfter = [ M || #user_generated_log_entry{event_message=M} <- LogsAfter ],
+
+ {_, {AfterHour, AfterMin, AfterSec}} = calendar:now_to_datetime(erlang:timestamp()),
+
+ io:fwrite("Logs after ~p→~p: ~p~n", [{StartHour, StartMin, StartSec}, {AfterHour, AfterMin, AfterSec}, MsgsAfter]),
+ ?assertMatch([ <<"after">>], MsgsAfter),
+ ok.
diff --git a/backend/apps/automate_bot_engine/test/just_wait_program.hrl b/backend/apps/automate_bot_engine/test/just_wait_program.hrl
index 2277e154..6779404f 100644
--- a/backend/apps/automate_bot_engine/test/just_wait_program.hrl
+++ b/backend/apps/automate_bot_engine/test/just_wait_program.hrl
@@ -1,14 +1,6 @@
-define(JUST_WAIT_PROGRAM_TYPE, <<"scratch_program">>).
-define(JUST_WAIT_MONITOR_ID, <<"__just_wait_monitor_id__">>).
--define(JUST_WAIT_PROGRAM_TRIGGER, #{ ?ARGUMENTS =>
- #{ ?MONITOR_ID => ?JUST_WAIT_MONITOR_ID
- , ?MONITOR_EXPECTED_VALUE => #{ ?TYPE => ?VARIABLE_CONSTANT
- , ?VALUE => start
- }
- }
- , ?TYPE => ?WAIT_FOR_MONITOR}).
-
-define(JUST_WAIT_PROGRAM_INSTRUCTIONS, [#{<<"args">> => [#{<<"type">> => <<"constant">>,
<<"value">> => <<"10">>}]
, <<"contents">> => []
@@ -17,12 +9,3 @@
-define(JUST_WAIT_PROGRAM_VARIABLES, []).
-define(JUST_WAIT_PROGRAM_ORIG, <<"">>).
-
--define(JUST_WAIT_PROGRAM_INITIALIZATION,
- #program_state{ program_id=?JUST_WAIT_PROGRAM_ID
- , variables=?JUST_WAIT_PROGRAM_VARIABLES
- , triggers=[#program_trigger{ condition=?JUST_WAIT_PROGRAM_TRIGGER
- , subprogram=?JUST_WAIT_PROGRAM_INSTRUCTIONS
- }
- ]
- }).
diff --git a/backend/apps/automate_bot_engine/test/single_line_program.hrl b/backend/apps/automate_bot_engine/test/single_line_program.hrl
index b0e18d87..9b374c36 100644
--- a/backend/apps/automate_bot_engine/test/single_line_program.hrl
+++ b/backend/apps/automate_bot_engine/test/single_line_program.hrl
@@ -105,7 +105,7 @@
-define(SINGLE_LINE_PROGRAM,
#user_program_entry{ id=?SINGLE_LINE_PROGRAM_ID
- , user_id=?SINGLE_LINE_PROGRAM_USER_ID
+ , owner={user, ?SINGLE_LINE_PROGRAM_USER_ID}
, program_name=?SINGLE_LINE_PROGRAM_NAME
, program_type=?SINGLE_LINE_PROGRAM_TYPE
, program_parsed=#{ <<"blocks">> => [[ ?SINGLE_LINE_PROGRAM_TRIGGER
diff --git a/backend/apps/automate_channel_engine/src/automate_channel_engine.app.src b/backend/apps/automate_channel_engine/src/automate_channel_engine.app.src
index bcd5cbc9..cdc9f7ef 100644
--- a/backend/apps/automate_channel_engine/src/automate_channel_engine.app.src
+++ b/backend/apps/automate_channel_engine/src/automate_channel_engine.app.src
@@ -8,6 +8,7 @@
, automate_storage
, automate_stats
, automate_configuration
+ , automate_coordination
]},
{env, [
]},
diff --git a/backend/apps/automate_channel_engine/src/automate_channel_engine.erl b/backend/apps/automate_channel_engine/src/automate_channel_engine.erl
index 1ba454d7..16e21990 100644
--- a/backend/apps/automate_channel_engine/src/automate_channel_engine.erl
+++ b/backend/apps/automate_channel_engine/src/automate_channel_engine.erl
@@ -7,14 +7,17 @@
%% Public API
-export([ create_channel/0
+ , delete_channel/1
, listen_channel/1
, listen_channel/2
, send_to_channel/2
+ , send_to_process/2
, monitor_listeners/3
, get_listeners_on_channel/1
]).
-include("records.hrl").
-define(LOGGING, automate_logging).
+-define(MONITOR, automate_channel_engine_listener_monitor).
%%====================================================================
%% API
@@ -29,20 +32,32 @@ create_channel() ->
X
end.
+-spec delete_channel(binary()) -> ok | {error, atom()}.
+delete_channel(ChannelId) ->
+ automate_channel_engine_mnesia_backend:delete_channel(ChannelId).
+
-spec listen_channel(binary()) -> ok | {error, channel_not_found}.
listen_channel(ChannelId) ->
- automate_channel_engine_mnesia_backend:add_listener_to_channel(ChannelId, self(), node(), undefined, undefined).
-
--spec listen_channel(binary(), {binary()} | {binary(), binary()}) -> ok | {error, channel_not_found}.
-listen_channel(ChannelId, {Key, SubKey}) ->
- automate_channel_engine_mnesia_backend:add_listener_to_channel(ChannelId, self(), node(), Key, SubKey);
+ listen_channel(ChannelId, {undefined, undefined}).
+-spec listen_channel(binary(), {binary()} | {binary() | common_channel_keys(), binary() | undefined}) -> ok | {error, channel_not_found}.
listen_channel(ChannelId, {Key}) ->
- automate_channel_engine_mnesia_backend:add_listener_to_channel(ChannelId, self(), node(), Key, undefined).
+ listen_channel(ChannelId, {Key, undefined});
--spec send_to_channel(binary(), any()) -> ok.
+listen_channel(ChannelId, {Key, SubKey}) ->
+ Pid = self(),
+ Node = node(),
+ case automate_channel_engine_mnesia_backend:add_listener_to_channel(ChannelId, Pid, Node,
+ automate_channel_engine_utils:canonicalize_selector(Key),
+ automate_channel_engine_utils:canonicalize_selector(SubKey)) of
+ ok ->
+ ?MONITOR:monitor_listener(Pid);
+ Error ->
+ Error
+ end.
+
+-spec send_to_channel(binary(), any()) -> ok | {error, channel_not_found }.
send_to_channel(ChannelId, Message) ->
- %% TODO: Use the key/subkey information to better route calls
spawn(fun () ->
automate_stats:log_observation(counter,
automate_channel_engine_messages_in,
@@ -50,22 +65,38 @@ send_to_channel(ChannelId, Message) ->
?LOGGING:log_event(ChannelId, Message)
end),
- case automate_channel_engine_mnesia_backend:get_listeners_on_channel(ChannelId) of
- {ok, Listeners} ->
- %% io:format("Forwarding ~p to ~p~n", [Message, Listeners]),
- lists:foreach(fun(#listeners_table_entry{pid=Pid}) ->
+ case get_appropriate_listeners(ChannelId, Message) of
+ {ok, []} ->
+ ok;
+
+ {ok, UniquePids} ->
+ lists:foreach(fun(Pid) ->
Pid ! {channel_engine, ChannelId, Message},
spawn(fun () ->
automate_stats:log_observation(counter,
automate_channel_engine_messages_out,
[ChannelId])
end)
- end, Listeners),
+ end, UniquePids),
ok;
X ->
X
end.
+-spec send_to_process(pid(), any()) -> ok.
+send_to_process(Pid, Message) ->
+ spawn(fun () ->
+ automate_stats:log_observation(counter,
+ automate_channel_engine_messages_in,
+ [<<"direct">>]),
+ automate_stats:log_observation(counter,
+ automate_channel_engine_messages_out,
+ [<<"direct">>])
+ end),
+
+ Pid ! {channel_engine, direct, Message},
+ ok.
+
-spec monitor_listeners(binary(), pid(), node()) -> ok | {error, channel_not_found}.
monitor_listeners(ChannelId, Pid, Node) ->
automate_channel_engine_mnesia_backend:add_listener_monitor(ChannelId, Pid, Node).
@@ -85,3 +116,38 @@ get_listeners_on_channel(ChannelId) ->
%%====================================================================
generate_id() ->
binary:list_to_bin(uuid:to_string(uuid:uuid4())).
+
+get_appropriate_listeners(ChannelId, #{ <<"key">> := Key, <<"subkey">> := SubKey }) ->
+ get_appropriate_listeners_key_subkey(ChannelId, {Key, SubKey});
+get_appropriate_listeners(ChannelId, #{ <<"key">> := Key }) ->
+ get_appropriate_listeners_key_subkey(ChannelId, {Key, null});
+get_appropriate_listeners(ChannelId, _Message) ->
+ get_appropriate_listeners_key_subkey(ChannelId, {null, null}).
+
+get_appropriate_listeners_key_subkey(ChannelId, {Key, SubKey}) ->
+ case automate_channel_engine_mnesia_backend:get_listeners_on_channel(ChannelId) of
+ {error, Reason } ->
+ {error, Reason};
+ {ok, Listeners} ->
+ CanonicalKey = automate_channel_engine_utils:canonicalize_selector(Key),
+ CanonicalSubKey = automate_channel_engine_utils:canonicalize_selector(SubKey),
+ Uniques = sets:from_list(lists:filtermap(fun(#listeners_table_entry{pid=Pid, key=ListenerKey, subkey=ListenerSubKey}) ->
+ AcceptedKey = case ListenerKey of
+ CanonicalKey -> true;
+ undefined -> true;
+ _ -> false
+ end,
+ AcceptedSubKey = case ListenerSubKey of
+ CanonicalSubKey -> true;
+ undefined -> true;
+ _ -> false
+ end,
+ case AcceptedKey and AcceptedSubKey of
+ true ->
+ {true, Pid};
+ false -> false
+ end
+ end,
+ Listeners)),
+ {ok, sets:to_list(Uniques)}
+ end.
diff --git a/backend/apps/automate_channel_engine/src/automate_channel_engine_listener_monitor.erl b/backend/apps/automate_channel_engine/src/automate_channel_engine_listener_monitor.erl
new file mode 100644
index 00000000..abb47b66
--- /dev/null
+++ b/backend/apps/automate_channel_engine/src/automate_channel_engine_listener_monitor.erl
@@ -0,0 +1,73 @@
+%%%-------------------------------------------------------------------
+%% @doc automate_channel_engine_listener_monitor public API
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(automate_channel_engine_listener_monitor).
+
+-export([ start_link/0
+ , monitor_listener/1
+ ]).
+
+-include("records.hrl").
+-define(BACKEND, automate_channel_engine_mnesia_backend).
+
+%%====================================================================
+%% API
+%%====================================================================
+start_link() ->
+ case automate_coordination:run_task_not_parallel(
+ fun() ->
+ yes = global:re_register_name(?MODULE, self()),
+ process_flag(trap_exit, true),
+ loop()
+ end, ?MODULE) of
+ {started, Pid} ->
+ link(Pid),
+ {ok, Pid};
+ {already_running, Pid} ->
+ link(Pid),
+ {ok, Pid};
+ {error, Error} ->
+ {error, Error}
+ end.
+
+
+-spec monitor_listener(pid()) -> ok.
+monitor_listener(Pid) ->
+ global:send(?MODULE, { monitor, self(), Pid }),
+ receive { ?MODULE, Response } ->
+ Response
+ end.
+
+%%====================================================================
+%% Private API
+%%====================================================================
+loop() ->
+ receive
+ { monitor, Answer, Pid } ->
+ %% A link() is used here instead of a monitor to avoid a memory
+ %% leak-like behavior when the same Pid is monitored again and
+ %% again.
+ %%
+ %% Actually here the monitoring is not bidirectional (the listener
+ %% process doesn't care about what happens with this daemon) but as
+ %% errors here should be fairly unusual and adding de-duplication
+ %% logic to this process will make it more complex we will skip it for now.
+ erlang:link(Pid),
+ case Answer of
+ _ when is_pid(Answer) ->
+ Answer ! { ?MODULE, ok };
+ _ ->
+ ok
+ end,
+ loop();
+ { 'EXIT', Pid, _Reason } ->
+ ok = ?BACKEND:remove_listener(Pid),
+ loop();
+ stop ->
+ ok;
+ Msg ->
+ automate_logging:log_platform(warning, io_lib:format("Unknown message on listener monitor: ~p", [Msg])),
+ loop()
+ end.
diff --git a/backend/apps/automate_channel_engine/src/automate_channel_engine_mnesia_backend.erl b/backend/apps/automate_channel_engine/src/automate_channel_engine_mnesia_backend.erl
index 6062b1d7..584131f1 100644
--- a/backend/apps/automate_channel_engine/src/automate_channel_engine_mnesia_backend.erl
+++ b/backend/apps/automate_channel_engine/src/automate_channel_engine_mnesia_backend.erl
@@ -7,13 +7,17 @@
-export([ start_link/0
, register_channel/1
+ , delete_channel/1
, add_listener_to_channel/5
, get_listeners_on_channel/1
, add_listener_monitor/3
+ , remove_listener/1
+ , exists_channel/1
]).
-include("records.hrl").
-include("databases.hrl").
+-define(MSG_PREFIX, automate_channel_engine).
%%====================================================================
%% API
@@ -30,6 +34,7 @@ start_link() ->
[ { attributes, record_info(fields, listeners_table_entry)}
, { ram_copies, Nodes }
, { record_name, listeners_table_entry }
+ , { index, [ pid ] }
, { type, bag }
]) of
{ atomic, ok } ->
@@ -37,6 +42,8 @@ start_link() ->
{ aborted, { already_exists, _ }} ->
ok
end,
+ ok = mnesia:wait_for_tables([ ?LISTENERS_TABLE
+ ], automate_configuration:get_table_wait_time()),
ok = case mnesia:create_table(?MONITORS_TABLE,
[ { attributes, record_info(fields, monitors_table_entry)}
@@ -51,7 +58,7 @@ start_link() ->
end,
ok = mnesia:wait_for_tables([ ?LIVE_CHANNELS_TABLE
- , ?LISTENERS_TABLE
+ , ?MONITORS_TABLE
], automate_configuration:get_table_wait_time()),
ignore.
@@ -69,6 +76,19 @@ register_channel(ChannelId) ->
{error, Reason, mnesia:error_description(Reason)}
end.
+-spec delete_channel(binary()) -> ok | {error, term(), string()}.
+delete_channel(ChannelId) ->
+ Transaction = fun() ->
+ ok = mnesia:delete(?LIVE_CHANNELS_TABLE, ChannelId, write)
+ end,
+
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ {error, Reason}
+ end.
+
-spec add_listener_to_channel(binary(), pid(), node(), binary() | undefined, binary() | undefined) -> ok | {error, channel_not_found}.
add_listener_to_channel(ChannelId, Pid, Node, Key, SubKey) ->
Transaction = fun() ->
@@ -91,7 +111,7 @@ add_listener_to_channel(ChannelId, Pid, Node, Key, SubKey) ->
ok ->
case get_monitors_on_channel(ChannelId) of
{ok, Monitors} ->
- [ MonPid ! { automate_channel_engine, add_listener, { Pid, Key, SubKey } }
+ [ MonPid ! { ?MSG_PREFIX, add_listener, { Pid, Key, SubKey } }
|| #monitors_table_entry{pid=MonPid} <- Monitors ],
ok
end;
@@ -99,6 +119,48 @@ add_listener_to_channel(ChannelId, Pid, Node, Key, SubKey) ->
Result
end.
+-spec remove_listener(Pid :: pid()) -> ok | {error, any()}.
+remove_listener(Pid) ->
+ T = (fun() ->
+ %% Obtain entries
+ PresentInChannels = mnesia:index_read(?LISTENERS_TABLE, Pid, #listeners_table_entry.pid),
+
+ %% Remove from all entries, obtain the channels
+ Channels = lists:map(fun(Rec=#listeners_table_entry{live_channel_id=Channel}) ->
+ ok = mnesia:delete_object(?LISTENERS_TABLE, Rec, write),
+ Channel
+ end, PresentInChannels),
+
+ %% Get the channel's monitors
+ UniqueChannels = sets:from_list(Channels),
+ Monitors = sets:fold(fun(Channel, Acc) ->
+ Monitors = mnesia:read(?MONITORS_TABLE, Channel),
+ lists:map(fun(#monitors_table_entry{pid=MonPid}) -> {MonPid, Channel} end,
+ Monitors) ++ Acc
+ end, [], UniqueChannels),
+ {ok, Monitors}
+ end),
+ case mnesia:transaction(T) of
+ {atomic, {ok, Monitors}} ->
+ [ MonPid ! { ?MSG_PREFIX, remove_listener, { Pid, MonChannel } }
+ || { MonPid, MonChannel } <- Monitors],
+ ok;
+ {atomic, Error} ->
+ Error;
+ Error ->
+ { error, Error }
+ end.
+
+-spec exists_channel(binary()) -> true | false.
+exists_channel(ChannelId) ->
+ T = fun() ->
+ case mnesia:read(?LIVE_CHANNELS_TABLE, ChannelId) of
+ [] -> false;
+ [ _ ] -> true
+ end
+ end,
+ mnesia:activity(ets, T).
+
-spec get_listeners_on_channel(binary()) -> {ok, [#listeners_table_entry{}]}
| {error, channel_not_found}.
get_listeners_on_channel(ChannelId) ->
@@ -120,14 +182,11 @@ get_listeners_on_channel(ChannelId) ->
end
end,
- {atomic, Result} = mnesia:transaction(Transaction),
+ Result = mnesia:ets(Transaction),
case Result of
{error, Error} -> {error, Error};
- {ok, Listeners} ->
- {AliveListeners, DeadListeners} = check_alive_listeners(Listeners),
- lists:foreach(fun unlink_listener/1, DeadListeners),
- {ok, AliveListeners}
+ {ok, Listeners} -> {ok, Listeners}
end.
-spec add_listener_monitor(binary(), pid(), node()) -> ok | {error, channel_not_found}.
@@ -170,7 +229,7 @@ get_monitors_on_channel(ChannelId) ->
end
end,
- {atomic, Result} = mnesia:transaction(Transaction),
+ Result = mnesia:ets(Transaction),
case Result of
{error, Error} -> {error, Error};
@@ -180,24 +239,10 @@ get_monitors_on_channel(ChannelId) ->
{ok, AliveMonitors}
end.
-
--spec unlink_listener(#listeners_table_entry{}) -> ok.
-unlink_listener(Entry) ->
- mnesia:dirty_delete(?LISTENERS_TABLE, Entry).
-
-spec unlink_monitor(#monitors_table_entry{}) -> ok.
unlink_monitor(Entry) ->
mnesia:dirty_delete(?MONITORS_TABLE, Entry).
--spec check_alive_listeners([#listeners_table_entry{}]) -> { [#listeners_table_entry{}]
- , [#listeners_table_entry{}]
- }.
-check_alive_listeners(Connections) ->
- lists:partition(fun(#listeners_table_entry{pid=Pid, node=Node }) ->
- automate_coordination_utils:is_process_alive(Pid, Node)
- end,
- Connections).
-
-spec check_alive_monitors([#monitors_table_entry{}]) -> { [#monitors_table_entry{}]
, [#monitors_table_entry{}]
}.
diff --git a/backend/apps/automate_channel_engine/src/automate_channel_engine_sup.erl b/backend/apps/automate_channel_engine/src/automate_channel_engine_sup.erl
index 3566f2e0..ec443fb2 100644
--- a/backend/apps/automate_channel_engine/src/automate_channel_engine_sup.erl
+++ b/backend/apps/automate_channel_engine/src/automate_channel_engine_sup.erl
@@ -29,7 +29,7 @@ start_link() ->
%% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules}
init([]) ->
- {ok, { {one_for_one, ?AUTOMATE_SUPERVISOR_INTENSITY, ?AUTOMATE_SUPERVISOR_PERIOD},
+ {ok, { {rest_for_one, ?AUTOMATE_SUPERVISOR_INTENSITY, ?AUTOMATE_SUPERVISOR_PERIOD},
[ #{ id => automate_channel_engine_mnesia_backend
, start => {automate_channel_engine_mnesia_backend, start_link, []}
, restart => permanent
@@ -37,6 +37,13 @@ init([]) ->
, type => worker
, modules => [automate_channel_engine_mnesia_backend]
}
+ , #{ id => automate_channel_engine_listener_monitor
+ , start => {automate_channel_engine_listener_monitor, start_link, []}
+ , restart => permanent
+ , shutdown => 2000
+ , type => worker
+ , modules => [automate_channel_engine_listener_monitor]
+ }
]} }.
%%====================================================================
diff --git a/backend/apps/automate_channel_engine/src/automate_channel_engine_utils.erl b/backend/apps/automate_channel_engine/src/automate_channel_engine_utils.erl
new file mode 100644
index 00000000..96e3e0a2
--- /dev/null
+++ b/backend/apps/automate_channel_engine/src/automate_channel_engine_utils.erl
@@ -0,0 +1,9 @@
+-module(automate_channel_engine_utils).
+
+-export([ canonicalize_selector/1
+ ]).
+
+canonicalize_selector(Atom) when is_atom(Atom) or is_tuple(Atom) ->
+ Atom;
+canonicalize_selector(Selector) when is_binary(Selector) or is_list(Selector) ->
+ string:lowercase(Selector).
diff --git a/backend/apps/automate_channel_engine/src/records.hrl b/backend/apps/automate_channel_engine/src/records.hrl
index 0ada3951..baedb437 100644
--- a/backend/apps/automate_channel_engine/src/records.hrl
+++ b/backend/apps/automate_channel_engine/src/records.hrl
@@ -2,6 +2,8 @@
-define(CHANNEL_MESSAGE_CONTENT, <<"content">>).
+-type common_channel_keys() :: undefined | ui_events | ui_events_show | variable_events | block_run_events.
+
-record(live_channels_table_entry, { live_channel_id :: binary()
, stats :: [_]
}).
diff --git a/backend/apps/automate_channel_engine/test/automate_channel_engine_tests.erl b/backend/apps/automate_channel_engine/test/automate_channel_engine_tests.erl
index 2d55359d..feea3088 100644
--- a/backend/apps/automate_channel_engine/test/automate_channel_engine_tests.erl
+++ b/backend/apps/automate_channel_engine/test/automate_channel_engine_tests.erl
@@ -47,7 +47,7 @@ tests(_SetupResult) ->
[ {"[Channel creation] Create two channels, IDs are different", fun channel_creation_different_names/0}
, {"[Message sending] Register on a channel, message it", fun simple_listen_send/0}
, {"[Message sending] Register twice on a channel, message it", fun simple_double_listen_send/0}
- , {"[Housekeeping] Register on on a channel, then close", fun register_and_close/0}
+ , {"[Monitoring] Monitor listeners added and removed", fun monitor_listeners_added_and_removed/0}
, {"[Errors] Channel not found on listening", fun channel_not_found_on_listening/0}
, {"[Errors] Channel not found on sending", fun channel_not_found_on_sending/0}
].
@@ -98,21 +98,40 @@ simple_double_listen_send() ->
ok
end.
-%%%% Housekeeping
-register_and_close() ->
+%% Monitoring
+monitor_listeners_added_and_removed() ->
{ok, ChannelId} = automate_channel_engine:create_channel(),
process_flag(trap_exit, true),
+ ok = automate_channel_engine:monitor_listeners(ChannelId, self(), node()),
+
Pid = spawn_link(fun() ->
- ok = automate_channel_engine:listen_channel(ChannelId)
+ ok = automate_channel_engine:listen_channel(ChannelId),
+ receive disconnect ->
+ ok
+ end
end),
+ receive { automate_channel_engine, add_listener, {Pid, _Key, _SubKey}} ->
+ ok
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+
+ Pid ! disconnect,
+
receive {'EXIT', Pid, normal} ->
ok
after ?RECEIVE_TIMEOUT ->
ct:fail(timeout)
end,
+ receive { automate_channel_engine, remove_listener, {Pid, ChannelId}} ->
+ ok
+ after ?FAST_RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+
Result = automate_channel_engine_mnesia_backend:get_listeners_on_channel(ChannelId),
?assertMatch({ok, []}, Result).
diff --git a/backend/apps/automate_common_types/src/limits.hrl b/backend/apps/automate_common_types/src/limits.hrl
new file mode 100644
index 00000000..aa0c10a3
--- /dev/null
+++ b/backend/apps/automate_common_types/src/limits.hrl
@@ -0,0 +1 @@
+-define(USER_PROGRAM_MAX_VAR_SIZE, 1048576). %% 1MB
diff --git a/backend/apps/automate_common_types/src/protocol.hrl b/backend/apps/automate_common_types/src/protocol.hrl
new file mode 100644
index 00000000..23a4a000
--- /dev/null
+++ b/backend/apps/automate_common_types/src/protocol.hrl
@@ -0,0 +1,7 @@
+-ifndef(AUTOMATE_COMMON_PROTO_IMPORTED).
+-define(AUTOMATE_COMMON_PROTO_IMPORTED, true).
+
+-define(PROTO_ON_BRIDGE_CONNECTED, '__proto_on_bridge_connected').
+-define(PROTO_ON_BRIDGE_DISCONNECTED, '__proto_on_bridge_disconnected').
+
+-endif.
diff --git a/backend/apps/automate_common_types/src/types.hrl b/backend/apps/automate_common_types/src/types.hrl
index 3a04a4aa..012f9532 100644
--- a/backend/apps/automate_common_types/src/types.hrl
+++ b/backend/apps/automate_common_types/src/types.hrl
@@ -1,3 +1,16 @@
-ifndef(MNESIA_SELECTOR).
-define(MNESIA_SELECTOR, '_' | '$1' | '$2' | '$3' | '$4').
-endif.
+
+-ifndef(COMMON_TYPES).
+
+%% Defining user_id() and group_id() led to error before.
+%% Better to just use owner_id().
+-type owner_id() :: { user, binary() } | { group, binary() }.
+-type user_program_visibility() :: public | private | shareable.
+-type thread_direction() :: forward | up.
+
+-define(COMMON_TYPES, defined).
+
+-define(OWNER_ID_MNESIA_SELECTOR, { user, ?MNESIA_SELECTOR } | { group, ?MNESIA_SELECTOR } | { ?MNESIA_SELECTOR, ?MNESIA_SELECTOR }).
+-endif.
diff --git a/backend/apps/automate_configuration/priv/assets/public/groups/.gitignore b/backend/apps/automate_configuration/priv/assets/public/groups/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/backend/apps/automate_configuration/priv/assets/public/groups/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/backend/apps/automate_configuration/priv/assets/public/icons/.gitignore b/backend/apps/automate_configuration/priv/assets/public/icons/.gitignore
new file mode 100644
index 00000000..72e8ffc0
--- /dev/null
+++ b/backend/apps/automate_configuration/priv/assets/public/icons/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/backend/apps/automate_configuration/priv/assets/public/users/.gitignore b/backend/apps/automate_configuration/priv/assets/public/users/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/backend/apps/automate_configuration/priv/assets/public/users/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/backend/apps/automate_configuration/src/automate_configuration.erl b/backend/apps/automate_configuration/src/automate_configuration.erl
index c1e35770..e0ab0554 100644
--- a/backend/apps/automate_configuration/src/automate_configuration.erl
+++ b/backend/apps/automate_configuration/src/automate_configuration.erl
@@ -6,25 +6,50 @@
-module(automate_configuration).
-export([ get_table_wait_time/0
+ , get_program_logs_watermarks/0
+ , get_frontend_root_url/0
+ , get_backend_api_info/0
, get_sync_peers/0
, get_sync_primary/0
, is_node_primary/1
+ , asset_directory/1
]).
-define(APPLICATION, automate).
-define(DEFAULT_WAIT_TIME, 10000).
+-define(DEFAULT_USER_PROGRAM_LOGS_LOW_WATERMARK, 1000).
+-define(DEFAULT_USER_PROGRAM_LOGS_HIGH_WATERMARK, 2000).
-define(SYNC_PRIMARY_ENV_VARIABLE, "AUTOMATE_SYNC_PRIMARY").
-define(SYNC_PEERS_ENV_VARIABLE, "AUTOMATE_SYNC_PEERS").
-define(SYNC_PEERS_SPLIT_TOKEN, ",").
%%====================================================================
-%% Utils functions
+%% Utils functions
%%====================================================================
-spec get_table_wait_time() -> non_neg_integer().
get_table_wait_time() ->
application:get_env(?APPLICATION, table_wait_time, ?DEFAULT_WAIT_TIME).
+-spec get_program_logs_watermarks() -> {non_neg_integer(), non_neg_integer()}.
+get_program_logs_watermarks() ->
+ LowWatermark = application:get_env(?APPLICATION, user_program_logs_count_low_watermark, ?DEFAULT_USER_PROGRAM_LOGS_LOW_WATERMARK),
+ HighWatermark = application:get_env(?APPLICATION, user_program_logs_count_high_watermark, ?DEFAULT_USER_PROGRAM_LOGS_HIGH_WATERMARK),
+ case LowWatermark =< HighWatermark of
+ true -> {LowWatermark, HighWatermark};
+ false ->
+ erlang:halt("'user_program_logs_count_low_watermark' must be =< 'user_program_logs_count_high_watermark'"
+ , [{flush, false}])
+ end.
+
+-spec get_frontend_root_url() -> binary().
+get_frontend_root_url() ->
+ application:get_env(?APPLICATION, frontend_root_url, <<"/">>).
+
+-spec get_backend_api_info() -> #{ scheme => binary(), host => binary(), port => pos_integer() } | undefined.
+get_backend_api_info() ->
+ application:get_env(?APPLICATION, backend_api_info, undefined).
+
-spec get_sync_peers() -> [node()].
get_sync_peers() ->
case os:getenv(?SYNC_PEERS_ENV_VARIABLE) of
@@ -58,3 +83,13 @@ is_node_primary(Node) ->
end.
+-spec asset_directory(string() | binary()) -> binary().
+asset_directory(SubDir) ->
+ BaseDirectory = case application:get_env(?APPLICATION, asset_directory) of
+ undefined ->
+ code:lib_dir(automate_configuration, priv) ++ "/assets";
+ {ok, Value} ->
+ Value
+ end,
+ binary:list_to_bin(
+ lists:flatten(io_lib:format("~s/~s", [BaseDirectory, SubDir]))).
diff --git a/backend/apps/automate_configuration/src/automate_configuration_app.erl b/backend/apps/automate_configuration/src/automate_configuration_app.erl
index 6407a2b8..401f98c3 100644
--- a/backend/apps/automate_configuration/src/automate_configuration_app.erl
+++ b/backend/apps/automate_configuration/src/automate_configuration_app.erl
@@ -8,15 +8,21 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1, check_assertions/0]).
%%====================================================================
%% API
%%====================================================================
-start(_StartType, _StartArgs) ->
+start() ->
+ %% Check that configuration assertions are valid
+ check_assertions(),
automate_configuration_sup:start_link().
+
+start(_StartType, _StartArgs) ->
+ start().
+
%%--------------------------------------------------------------------
stop(_State) ->
ok.
@@ -24,3 +30,5 @@ stop(_State) ->
%%====================================================================
%% Internal functions
%%====================================================================
+check_assertions() ->
+ automate_configuration:get_program_logs_watermarks().
diff --git a/backend/apps/automate_coordination/src/automate_coordination_app.erl b/backend/apps/automate_coordination/src/automate_coordination_app.erl
index d106444e..12326dc1 100644
--- a/backend/apps/automate_coordination/src/automate_coordination_app.erl
+++ b/backend/apps/automate_coordination/src/automate_coordination_app.erl
@@ -8,14 +8,17 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
+start() ->
+ automate_coordination_sup:start_link().
+
start(_StartType, _StartArgs) ->
- automate_coordination_sup:start_link().
+ start().
%%--------------------------------------------------------------------
stop(_State) ->
diff --git a/backend/apps/automate_engines/src/automate_engines.app.src b/backend/apps/automate_engines/src/automate_engines.app.src
new file mode 100644
index 00000000..1532aede
--- /dev/null
+++ b/backend/apps/automate_engines/src/automate_engines.app.src
@@ -0,0 +1,26 @@
+{application, automate_engines, [
+ {description, "Auto-mate engine group."},
+ {vsn, "0.0.0"},
+ {registered, []},
+ {mod, { automate_app, [] }},
+ {applications, [ kernel
+ , stdlib
+ , automate_configuration
+ , automate_storage
+ ]},
+ {included_applications, [ automate_channel_engine
+ , automate_bot_engine
+ , automate_service_registry
+ , automate_monitor_engine
+ , automate_service_port_engine
+ , automate_template_engine
+ , automate_stats
+ , automate_mail
+ , automate_services_time
+ ]},
+ {env, [
+ ]},
+ {modules, []},
+ {licenses, ["Apache 2.0"]},
+ {links, []}
+]}.
diff --git a/backend/apps/automate_engines/src/automate_engines_app.erl b/backend/apps/automate_engines/src/automate_engines_app.erl
new file mode 100644
index 00000000..41349f68
--- /dev/null
+++ b/backend/apps/automate_engines/src/automate_engines_app.erl
@@ -0,0 +1,32 @@
+%%%-------------------------------------------------------------------
+%% @doc automate engines initialization
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(automate_engines_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/0, start/2, stop/1]).
+
+%%====================================================================
+%% API
+%%====================================================================
+start() ->
+ %% Dependencies
+ {ok, _} = application:ensure_all_started(prometheus),
+ %% Start supervisor
+ automate_engines_sup:start_link().
+
+
+start(_StartType, _StartArgs) ->
+ start().
+
+%%--------------------------------------------------------------------
+stop(_State) ->
+ ok.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
diff --git a/backend/apps/automate_engines/src/automate_engines_sup.erl b/backend/apps/automate_engines/src/automate_engines_sup.erl
new file mode 100644
index 00000000..b4c92070
--- /dev/null
+++ b/backend/apps/automate_engines/src/automate_engines_sup.erl
@@ -0,0 +1,94 @@
+%%%-------------------------------------------------------------------
+%% @doc automate engines supervisor.
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(automate_engines_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+-define(SERVER, ?MODULE).
+-include("../../automate_common_types/src/definitions.hrl").
+
+%%====================================================================
+%% API functions
+%%====================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+
+%%====================================================================
+%% Supervisor callbacks
+%%====================================================================
+
+%% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules}
+init([]) ->
+ {ok, { { one_for_one, ?AUTOMATE_SUPERVISOR_INTENSITY, ?AUTOMATE_SUPERVISOR_PERIOD},
+ [ #{ id => automate_channel_engine
+ , start => { automate_channel_engine_sup, start_link, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_channel_engine]
+ }
+ , #{ id => automate_bot_engine
+ , start => { automate_bot_engine_sup, start_link, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_bot_engine]
+ }
+ , #{ id => automate_service_registry
+ , start => { automate_service_registry_sup, start_link, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_service_registry]
+ }
+ , #{ id => automate_monitor_engine
+ , start => { automate_monitor_engine_sup, start_link, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_monitor_engine]
+ }
+ , #{ id => automate_service_port_engine
+ , start => { automate_service_port_engine_sup, start_link, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_service_port_engine]
+ }
+ , #{ id => automate_template_engine
+ , start => { automate_template_engine_app, start, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_template_engine]
+ }
+ , #{ id => automate_stats
+ , start => { automate_stats_app, start, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_stats]
+ }
+ %% Automate_mail does not have a supervisor (or long running process)
+ , #{ id => automate_services_time
+ , start => { automate_services_time_app, start, [] }
+ , restart => permanent
+ , shutdown => 2000
+ , type => supervisor
+ , modules => [automate_services_time]
+ }
+ ]} }.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
diff --git a/backend/apps/automate_logging/src/automate_logging.erl b/backend/apps/automate_logging/src/automate_logging.erl
index eb513b74..e6ec4ff0 100644
--- a/backend/apps/automate_logging/src/automate_logging.erl
+++ b/backend/apps/automate_logging/src/automate_logging.erl
@@ -6,8 +6,23 @@
-module(automate_logging).
%% Application callbacks
--export([log_event/2, log_call_to_bridge/5]).
+-export([ log_event/2
+ , log_signal_to_bridge_and_owner/3
+ , get_signal_by_bridge_and_owner_history/2
+ , log_program_call_by_user/2
+ , log_call_to_bridge/5
+ , log_program_error/1
+ , add_user_generated_program_log/1
+ , log_platform/4
+ , log_platform/2
+ , log_bridge/2
+ , log_api/3
+ ]).
+-define(DEFAULT_LOG_HISTORY_RETRIEVE, 1000).
+-include("../../automate_storage/src/records.hrl").
+-include("../../automate_bot_engine/src/program_records.hrl").
+-include("./records.hrl").
%%====================================================================
%% Logging API
@@ -52,6 +67,87 @@ log_event(Channel, Message) ->
ok
end.
+-spec log_signal_to_bridge_and_owner(Signal :: any(), BridgeId :: binary(), Owner :: owner_id()) -> ok.
+log_signal_to_bridge_and_owner(Signal, BridgeId, {OwnerType, OwnerId}) ->
+ Config = get_signal_storage_config(),
+ case Config of
+ #{ type := raw
+ , url := BaseURL
+ } ->
+ Url = lists:flatten(io_lib:format("~s/~s_~p_~s", [BaseURL, BridgeId, OwnerType, OwnerId])),
+ Type = "application/json",
+ Body = list_to_binary([jiffy:encode(Signal)]),
+ Headers = [],
+ HTTPOptions = [],
+ Options = [],
+ case httpc:request(post, {Url, Headers, Type, Body}, HTTPOptions, Options) of
+ {ok, _} -> ok;
+ {error, Reason} ->
+ log_platform(error, list_to_binary(io_lib:format("Error logging signal: ~p", [Reason])))
+ end;
+ undefined ->
+ io:fwrite("[Error] Signal logging configuration not set")
+ end.
+
+-spec get_signal_by_bridge_and_owner_history(BridgeId :: binary(), Owner :: owner_id()) -> {ok, iolist()} | {error, _}.
+get_signal_by_bridge_and_owner_history(BridgeId, {OwnerType, OwnerId}) ->
+ Config = get_signal_storage_config(),
+ case Config of
+ #{ type := raw
+ , url := BaseURL
+ } ->
+ Url = lists:flatten(io_lib:format("~s/~s_~p_~s?q=latest&n=~p", [BaseURL, BridgeId, OwnerType, OwnerId, ?DEFAULT_LOG_HISTORY_RETRIEVE])),
+ Headers = [],
+ HTTPOptions = [],
+ Options = [{body_format, binary}],
+ {ok, { {_, StatusCode, _StatusPhrase}, _Headers, Body }
+ } = httpc:request(get, {Url, Headers}, HTTPOptions, Options),
+ 2 = StatusCode div 100, %% Expect a 2XX status code.
+ { ok
+ , [<<"[">>, binary:replace(Body, <<"\0">>, <<",">>, [global]), <<"]">>]
+ };
+ undefined ->
+ {error, no_signal_logging};
+ none ->
+ {error, no_signal_logging}
+ end.
+
+-spec log_program_call_by_user(CallData :: #call_data{}, Owner :: owner_id() | 'none' | 'undefined') -> ok.
+log_program_call_by_user(CallData, undefined) ->
+ io:fwrite("[WARN] Cannot log call which is done by no user~n"),
+ ok;
+log_program_call_by_user(CallData, none) ->
+ io:fwrite("[WARN] Cannot log call which is done by no user~n"),
+ ok;
+log_program_call_by_user(CallData, {OwnerType, OwnerId}) ->
+ ProgramConfig = get_program_call_log_storage_config(),
+ case ProgramConfig of
+ #{ type := raw
+ , url := BaseURL
+ } ->
+ Url = lists:flatten(io_lib:format("~s/~p_~s", [BaseURL, OwnerType, OwnerId])),
+ Type = "application/json",
+ try
+ list_to_binary([jiffy:encode(to_map(CallData))])
+ of
+ Body ->
+ Headers = [],
+ HTTPOptions = [],
+ Options = [],
+ case httpc:request(post, {Url, Headers, Type, Body}, HTTPOptions, Options) of
+ {ok, _} -> ok;
+ {error, Reason} ->
+ log_platform(error, list_to_binary(io_lib:format("Error logging signal: ~p", [Reason])))
+ end
+ catch
+ ErrType:ErrReason:_ErrStack ->
+ io:fwrite("[Error] Preparing data to log signal: ~p~n", [{ErrType, ErrReason}])
+ end;
+ undefined ->
+ io:fwrite("[Error] Signal logging configuration not set~n");
+ none ->
+ io:fwrite("[WARN] Signal logging configuration not set~n")
+ end.
-spec log_call_to_bridge(binary(), binary(), binary(), binary(), map()) -> ok.
log_call_to_bridge(BridgeId, FunctionName, Arguments, UserId, ExtraData) ->
@@ -84,6 +180,61 @@ log_call_to_bridge(BridgeId, FunctionName, Arguments, UserId, ExtraData) ->
none -> ok
end.
+-spec log_program_error(#user_program_log_entry{}) -> ok | {error, atom()}.
+log_program_error(LogEntry=#user_program_log_entry{ severity=Severity, program_id=ProgramId }) ->
+ case automate_storage:get_program_from_id(ProgramId) of
+ {ok, #user_program_entry{ program_channel=Channel }} ->
+ automate_channel_engine:send_to_channel(Channel, LogEntry);
+ {error, not_found} ->
+ log_platform(Severity, io_lib:format(
+ "Cannot log error on program '~p', channel not found",
+ [ProgramId]))
+ end,
+
+ automate_storage:log_program_error(LogEntry).
+
+-spec add_user_generated_program_log(#user_generated_log_entry{}) -> ok | {error, atom()}.
+add_user_generated_program_log(LogEntry=#user_generated_log_entry{ program_id=ProgramId, severity=Severity }) ->
+ case automate_storage:get_program_from_id(ProgramId) of
+ {ok, #user_program_entry{ program_channel=Channel }} ->
+ automate_channel_engine:send_to_channel(Channel, LogEntry);
+ {error, not_found} ->
+ log_platform(Severity, io_lib:format(
+ "Cannot log error on program '~p', channel not found",
+ [ProgramId]))
+ end,
+
+ automate_storage:add_user_generated_log(LogEntry).
+
+
+-spec log_platform(log_severity(), _, _, _) -> ok.
+log_platform(warning, ErrorNS, Error, _StackTrace) ->
+ io:fwrite("~s [~p] ~p:~p~n", [get_time_string(), warning, ErrorNS, Error]);
+log_platform(debug, _ErrorNS, _Error, _StackTrace) ->
+ ok; %% Ignored for now
+
+log_platform(Severity, ErrorNS, Error, StackTrace) ->
+ io:fwrite("~s [~p] ~p:~p || ~p~n", [get_time_string(), Severity, ErrorNS, Error, StackTrace]).
+
+-spec log_platform(atom(), _) -> ok.
+log_platform(Severity, Msg) when is_list(Msg) ->
+ io:fwrite("~s [~p] ~s~n", [get_time_string(), Severity, binary:list_to_bin(lists:flatten(Msg))]);
+log_platform(Severity, Msg) ->
+ io:fwrite("~s [~p] ~p~n", [get_time_string(), Severity, Msg]).
+
+-spec log_bridge(log_severity(), iolist()) -> ok.
+log_bridge(Severity, Msg) ->
+ io:fwrite("~s [~p] ~s~n", [get_time_string(), Severity, binary:list_to_bin([Msg])]).
+
+-spec log_api(log_severity(), _, _) -> ok.
+log_api(debug, _, _) ->
+ ok; %% Ignored for now
+log_api(Severity, Endpoint, Error) when is_binary(Error) ->
+ io:fwrite("~s [~p@~p] ~s~n", [get_time_string(), Severity, Endpoint, Error]);
+log_api(Severity, Endpoint, Error) ->
+ io:fwrite("~s [~p@~p] ~p~n", [get_time_string(), Severity, Endpoint, Error]).
+
+
%%====================================================================
%% Internal functions
%%====================================================================
@@ -95,5 +246,52 @@ get_config() ->
none
end.
+get_signal_storage_config() ->
+ case application:get_env(automate_logging, signal_storage_endpoint) of
+ {ok, Config} ->
+ Config;
+ undefined ->
+ none
+ end.
+
+get_program_call_log_storage_config() ->
+ case application:get_env(automate_logging, program_call_log_storage_endpoint) of
+ {ok, Config} ->
+ Config;
+ undefined ->
+ none
+ end.
+
get_timestamp() ->
erlang:system_time(millisecond).
+
+get_time_string() ->
+ {{Year,Month,Day},{Hour,Min,Sec}} = erlang:localtime(),
+ io_lib:format("~4..0B/~2..0B/~2..0B ~2..0B:~2..0B:~2..0B", [Year, Month, Day, Hour, Min, Sec]).
+
+-spec to_map(#call_data{}) -> map().
+to_map(#call_data{ call_start_time=CallStartTime
+ , call_end_time=CallEndTime
+ , program_id=ProgramId
+ , operation=Operation
+ , arguments=Arguments
+ , result=Result
+ }) ->
+ #{ call_start_time => CallStartTime
+ , call_end_time => CallEndTime
+ , program_id => ProgramId
+ , operation => Operation
+ , arguments => case Arguments of
+ Tup when is_tuple(Tup) ->
+ tuple_to_list(Tup);
+ _ -> Arguments
+ end
+ , result => case Result of
+ #program_error{} ->
+ %% HACK: It's not ideal to require something at the
+ %% "bottom" of the module dependency graph from the top.
+ <<"error">>;
+ _ -> Result
+ end
+
+ }.
diff --git a/backend/apps/automate_logging/src/automate_logging_app.erl b/backend/apps/automate_logging/src/automate_logging_app.erl
index 1609fbbe..ca1c8831 100644
--- a/backend/apps/automate_logging/src/automate_logging_app.erl
+++ b/backend/apps/automate_logging/src/automate_logging_app.erl
@@ -8,14 +8,16 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
+start() ->
+ automate_logging_sup:start_link().
start(_StartType, _StartArgs) ->
- automate_logging_sup:start_link().
+ start().
%%--------------------------------------------------------------------
stop(_State) ->
diff --git a/backend/apps/automate_logging/src/records.hrl b/backend/apps/automate_logging/src/records.hrl
new file mode 100644
index 00000000..e4698fdf
--- /dev/null
+++ b/backend/apps/automate_logging/src/records.hrl
@@ -0,0 +1,15 @@
+-ifndef(AUTOMATE_LOGGING_RECORDS).
+-define(AUTOMATE_LOGGING_RECORDS, true).
+
+-record(call_data, { call_start_time :: time_in_seconds()
+ , call_end_time :: time_in_seconds()
+ , block_id :: binary()
+ , program_id :: binary()
+ , thread_id :: binary()
+ , succeeded :: boolean()
+ , operation :: binary()
+ , arguments :: [_] | tuple()
+ , result :: any()
+ }).
+
+-endif.
diff --git a/backend/apps/automate_mail/src/automate_mail.erl b/backend/apps/automate_mail/src/automate_mail.erl
index d1ff62d8..2a46ddbf 100644
--- a/backend/apps/automate_mail/src/automate_mail.erl
+++ b/backend/apps/automate_mail/src/automate_mail.erl
@@ -28,9 +28,31 @@ is_enabled() ->
-spec send_registration_verification(binary(), binary(), binary()) -> {ok, binary()} | {error, any()}.
send_registration_verification(ReceiverName, ReceiverMail, Code) ->
+ {ok, MailGateway} = application:get_env(?APPLICATION, mail_gateway),
+ case MailGateway of
+ { test_ets, EtsTable } ->
+ send_registration_verification_through_ets(ReceiverName, ReceiverMail, Code, EtsTable);
+ MailGateway ->
+ send_registration_verification_through_mail(ReceiverName, ReceiverMail, Code, MailGateway)
+ end.
+
+%% For debugging
+%% Note that for this to work, the ETS table must be public.
+send_registration_verification_through_ets(ReceiverName, ReceiverMail, Code, EtsTable) ->
+ Url = case application:get_env(?APPLICATION, registration_verification_url_pattern) of
+ {ok, UrlPattern} ->
+ binary:list_to_bin(
+ lists:flatten(io_lib:format(UrlPattern, [Code])));
+ undefined ->
+ none
+ end,
+ true = ets:insert(EtsTable, [{ ReceiverMail, ReceiverName, Code, Url }]),
+ {ok, Url}.
+
+-spec send_registration_verification_through_mail(binary(), binary(), binary(), binary()) -> {ok, binary()} | {error, any()}.
+send_registration_verification_through_mail(ReceiverName, ReceiverMail, Code, MailGateway) ->
{ok, Sender} = application:get_env(?APPLICATION, registration_verification_sender),
PlatformName = application:get_env(?APPLICATION, platform_name, ?DEFAULT_PLATFORM_NAME),
- {ok, MailGateway} = application:get_env(?APPLICATION, mail_gateway),
{ok, UrlPattern} = application:get_env(?APPLICATION, registration_verification_url_pattern),
Url = binary:list_to_bin(
lists:flatten(io_lib:format(UrlPattern, [Code]))),
diff --git a/backend/apps/automate_monitor_engine/src/automate_monitor_engine_runner.erl b/backend/apps/automate_monitor_engine/src/automate_monitor_engine_runner.erl
index db050317..57b02181 100644
--- a/backend/apps/automate_monitor_engine/src/automate_monitor_engine_runner.erl
+++ b/backend/apps/automate_monitor_engine/src/automate_monitor_engine_runner.erl
@@ -57,7 +57,7 @@ start_link(MonitorId) ->
init(MonitorId) ->
io:format("Starting ~p~n", [MonitorId]),
Monitor = automate_storage:get_monitor_from_id(MonitorId),
- timer:send_after(?FIRST_CHECK_INTERVAL, ?END_OF_INTERVAL_MESSAGE),
+ erlang:send_after(?FIRST_CHECK_INTERVAL, self(), ?END_OF_INTERVAL_MESSAGE),
loop(#state{ monitor=Monitor
}).
@@ -74,7 +74,7 @@ loop(State=#state{ monitor=Monitor
ok;
?END_OF_INTERVAL_MESSAGE ->
NextState = run(Monitor),
- timer:send_after(?CHECK_INTERVAL, ?END_OF_INTERVAL_MESSAGE),
+ erlang:send_after(?CHECK_INTERVAL, self(), ?END_OF_INTERVAL_MESSAGE),
loop(State#state{ monitor=NextState })
end.
diff --git a/backend/apps/automate_program_linker/src/automate_program_linker.app.src b/backend/apps/automate_program_linker/src/automate_program_linker.app.src
index 116e3a29..54d01df2 100644
--- a/backend/apps/automate_program_linker/src/automate_program_linker.app.src
+++ b/backend/apps/automate_program_linker/src/automate_program_linker.app.src
@@ -3,8 +3,8 @@
{description, "Auto-mate program linker."},
{vsn, "0.0.0"},
{registered, []},
- {applications, [ automate_channel_engine
- , automate_configuration
+ {applications, [ stdlib
+ , kernel
]},
{env, [
]},
diff --git a/backend/apps/automate_program_linker/src/automate_program_linker.erl b/backend/apps/automate_program_linker/src/automate_program_linker.erl
index 3e78957d..757433a6 100644
--- a/backend/apps/automate_program_linker/src/automate_program_linker.erl
+++ b/backend/apps/automate_program_linker/src/automate_program_linker.erl
@@ -10,78 +10,70 @@
%%====================================================================
%% API functions
%%====================================================================
--spec link_program(program(), binary()) -> {ok, program()}.
+-spec link_program(program(), owner_id()) -> {ok, program()}.
link_program(Program = #{ <<"blocks">> := Blocks },
- UserId) ->
- RelinkedBlocks = [relink_subprogram(Subprogram, UserId) || Subprogram <- Blocks],
+ Owner) ->
+ RelinkedBlocks = [relink_subprogram(Subprogram, Owner) || Subprogram <- Blocks],
{ok, Program#{ <<"blocks">> => RelinkedBlocks }}.
-relink_subprogram(Subprogram, UserId) ->
- [relink_block(Block, UserId) || Block <- Subprogram].
+relink_subprogram(Subprogram, Owner) ->
+ [relink_block(Block, Owner) || Block <- Subprogram].
%% Relink service monitor
-relink_block(Block, UserId) ->
- B1 = relink_block_contents(Block, UserId),
- B2 = relink_block_args(B1, UserId),
- B3 = relink_block_args_values(B2, UserId),
- relink_block_values(B3, UserId).
+relink_block(Block, Owner) ->
+ B1 = relink_block_contents(Block, Owner),
+ B2 = relink_block_args_values(B1, Owner),
+ relink_block_values(B2, Owner).
+
+relink_block_contents(Value = #{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := Arguments
+ }, Owner) ->
+ Value#{ ?ARGUMENTS => lists:map(fun(B) -> relink_block(B, Owner) end,
+ Arguments)
+ };
+
+relink_block_contents(Value = #{ ?TYPE := ?COMMAND_WAIT_FOR_NEXT_VALUE
+ , ?ARGUMENTS := Arguments
+ }, Owner) ->
+ Value#{ ?ARGUMENTS => lists:map(fun(B) -> relink_block(B, Owner) end,
+ Arguments)
+ };
relink_block_contents(Block=#{ ?CONTENTS := Contents
- }, UserId) when is_list(Contents) ->
- Block#{ ?CONTENTS => lists:map(fun(B) -> relink_block(B, UserId) end,
+ }, Owner) when is_list(Contents) ->
+ Block#{ ?CONTENTS => lists:map(fun(B) -> relink_block(B, Owner) end,
Contents)
};
-relink_block_contents(Block, _UserId) ->
- Block.
-
-relink_block_args(Block=#{ ?ARGUMENTS := Arguments
- }, UserId) when is_map(Arguments) ->
- B1 = relink_monitor_id(Block, UserId),
- B1;
-
-relink_block_args(Block, _UserId) ->
+relink_block_contents(Block, _Owner) ->
Block.
relink_block_args_values(Block=#{ ?ARGUMENTS := Arguments
- }, _UserId) when is_list(Arguments) ->
+ }, _Owner) when is_list(Arguments) ->
Block#{ ?ARGUMENTS := [ relink_value(Arg) || Arg <- Arguments ] };
-relink_block_args_values(Block, _UserId) ->
- Block.
-
-
-relink_monitor_id(Block=#{ ?ARGUMENTS := Args=
- #{ ?MONITOR_ID := #{ ?FROM_SERVICE := ServiceId } } }
- , UserId
- ) ->
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, MonitorId } = automate_service_registry_query:get_monitor_id(Module, UserId),
- Block#{ ?ARGUMENTS := Args#{ ?MONITOR_ID := MonitorId } };
-
-relink_monitor_id(Block, _UserId) ->
+relink_block_args_values(Block, _Owner) ->
Block.
-
%%%% Relink values
relink_block_values(Block=#{ ?VALUE := Value
- }, _UserId) when is_list(Value) ->
+ }, _Owner) when is_list(Value) ->
Block#{ ?VALUE => lists:map(fun relink_value/1, Value) };
-relink_block_values(Block, _UserId) ->
+relink_block_values(Block, _Owner) ->
Block.
-%% Relink time
+%% Relink UTC time (DEPR)
relink_value(Value = #{ ?TYPE := <<"time_get_utc_hour">>
}) ->
#{ ?TYPE => ?COMMAND_CALL_SERVICE
, ?ARGUMENTS => #{ ?SERVICE_ACTION => get_utc_hour
, ?SERVICE_ID => automate_services_time:get_uuid()
- , ?SERVICE_CALL_VALUES => Value
+ , ?SERVICE_CALL_VALUES => Value#{ <<"timezone">> => <<"UTC">> }
}
};
@@ -90,7 +82,7 @@ relink_value(Value = #{ ?TYPE := <<"time_get_utc_minute">>
#{ ?TYPE => ?COMMAND_CALL_SERVICE
, ?ARGUMENTS => #{ ?SERVICE_ACTION => get_utc_minute
, ?SERVICE_ID => automate_services_time:get_uuid()
- , ?SERVICE_CALL_VALUES => Value
+ , ?SERVICE_CALL_VALUES => Value#{ <<"timezone">> => <<"UTC">> }
}
};
@@ -98,13 +90,77 @@ relink_value(Value = #{ ?TYPE := <<"time_get_utc_seconds">>
}) ->
#{ ?TYPE => ?COMMAND_CALL_SERVICE
, ?ARGUMENTS => #{ ?SERVICE_ACTION => get_utc_seconds
+ , ?SERVICE_ID => automate_services_time:get_uuid()
+ , ?SERVICE_CALL_VALUES => Value#{ <<"timezone">> => <<"UTC">> }
+ }
+ };
+
+%% Relink Timezone time
+relink_value(Value = #{ ?TYPE := <<"time_get_tz_hour">>
+ }) ->
+ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ACTION => get_tz_hour
+ , ?SERVICE_ID => automate_services_time:get_uuid()
+ , ?SERVICE_CALL_VALUES => Value
+ }
+ };
+
+relink_value(Value = #{ ?TYPE := <<"time_get_tz_minute">>
+ }) ->
+ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ACTION => get_tz_minute
+ , ?SERVICE_ID => automate_services_time:get_uuid()
+ , ?SERVICE_CALL_VALUES => Value
+ }
+ };
+
+relink_value(Value = #{ ?TYPE := <<"time_get_tz_day_of_week">>
+ }) ->
+ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ACTION => get_tz_day_of_week
+ , ?SERVICE_ID => automate_services_time:get_uuid()
+ , ?SERVICE_CALL_VALUES => Value
+ }
+ };
+
+relink_value(Value = #{ ?TYPE := <<"time_get_tz_seconds">>
+ }) ->
+ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ACTION => get_tz_seconds
+ , ?SERVICE_ID => automate_services_time:get_uuid()
+ , ?SERVICE_CALL_VALUES => Value
+ }
+ };
+
+relink_value(Value = #{ ?TYPE := <<"time_get_tz_day_of_month">>
+ }) ->
+ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ACTION => get_tz_day_of_month
+ , ?SERVICE_ID => automate_services_time:get_uuid()
+ , ?SERVICE_CALL_VALUES => Value
+ }
+ };
+
+relink_value(Value = #{ ?TYPE := <<"time_get_tz_month_of_year">>
+ }) ->
+ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ACTION => get_tz_month_of_year
+ , ?SERVICE_ID => automate_services_time:get_uuid()
+ , ?SERVICE_CALL_VALUES => Value
+ }
+ };
+
+relink_value(Value = #{ ?TYPE := <<"time_get_tz_year">>
+ }) ->
+ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ACTION => get_tz_year
, ?SERVICE_ID => automate_services_time:get_uuid()
, ?SERVICE_CALL_VALUES => Value
}
};
%%%% ^^^ Service linking
-relink_value(Block=#{ ?ARGUMENTS := Arguments }) ->
+relink_value(Block=#{ ?ARGUMENTS := Arguments }) when is_list(Arguments) ->
Block#{ ?ARGUMENTS => lists:map(fun relink_value/1, Arguments) };
relink_value(Block=#{ ?VALUE := Values }) when is_list(Values) ->
diff --git a/backend/apps/automate_program_linker/src/records.hrl b/backend/apps/automate_program_linker/src/records.hrl
index 4f09099d..95b965d7 100644
--- a/backend/apps/automate_program_linker/src/records.hrl
+++ b/backend/apps/automate_program_linker/src/records.hrl
@@ -1,3 +1 @@
--define(FROM_SERVICE, <<"from_service">>).
-
-type program() :: map().
diff --git a/backend/apps/automate_program_linker/test/automate_program_linker_tests.erl b/backend/apps/automate_program_linker/test/automate_program_linker_tests.erl
new file mode 100644
index 00000000..f56c5e2e
--- /dev/null
+++ b/backend/apps/automate_program_linker/test/automate_program_linker_tests.erl
@@ -0,0 +1,45 @@
+%%% @doc
+%%% Automate channel engine tests.
+%%% @end
+
+-module(automate_program_linker_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(APPLICATION, automate_program_linker).
+-include("../../automate_bot_engine/src/instructions.hrl").
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ %% NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ %% {ok, Pid} = application:ensure_all_started(?APPLICATION),
+ {ok, _TimePid} = application:ensure_all_started(automate_services_time),
+
+ %% {NodeName, Pid}.
+ ok.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop(_) ->
+ %% application:stop(?APPLICATION),
+
+ ok.
+
+tests(_SetupResult) ->
+ [
+ ].
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api.app.src b/backend/apps/automate_rest_api/src/automate_rest_api.app.src
index e877a1bf..e983cc5e 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api.app.src
+++ b/backend/apps/automate_rest_api/src/automate_rest_api.app.src
@@ -6,16 +6,8 @@
, {applications, [ kernel
, stdlib
, ssl
- , inets
- , cowboy
, jiffy
- , automate_storage
- , automate_channel_engine
- , automate_stats
- , automate_service_port_engine
- , automate_template_engine
- , automate_configuration
- , automate_mail
+ , prometheus
]}
, {env, [ {port, 8888} ]}
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_admin_stats_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_admin_stats_root.erl
new file mode 100644
index 00000000..ed48f13b
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_admin_stats_root.erl
@@ -0,0 +1,144 @@
+%%% @doc
+%%% REST endpoint to retrieve platform stats.
+%%% @end
+
+-module(automate_rest_api_admin_stats_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , is_authorized/2
+ , content_types_provided/2
+ , options/2
+ ]).
+
+-export([ to_json/2
+ ]).
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_stats/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, Opts) ->
+ {cowboy_rest, Req, Opts}.
+
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, admin_read_stats) of
+ {true, UserId} ->
+ case automate_storage:get_user(UserId) of
+ {ok, #registered_user_entry{ is_admin=true }} ->
+ { true, Req1, State };
+ {ok, _} ->
+ { { false, <<"User not authorized (not admin)">>}, Req1, State };
+ {error, Reason} ->
+ automage_logging:log_api(error, ?MODULE, Reason)
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+%%%% GET
+-spec to_json(cowboy_req:req(),#rest_session{}) -> {binary(),cowboy_req:req(),_}.
+to_json(Req, Session) ->
+ {ok, Metrics, Errors} = automate_stats:get_internal_metrics(),
+ Output = jiffy:encode(#{ stats => serialize_stats(Metrics)
+ , errors => serialize_errors(Errors)
+ }),
+ Res = ?UTILS:send_json_format(Req),
+ { Output, Res, Session }.
+
+
+%% Serialization
+serialize_stats(#internal_metrics{ services_active=ServiceCounts
+ , bot_count=BotCount
+ , thread_count=ThreadCount
+ , monitor_count=MonitorCount
+ , service_count=ServiceCount
+ , user_stats=#user_stat_metrics{ count=UserCount
+ , registered_last_day=RegisteredUsersLastDay
+ , registered_last_week=RegisteredUsersLastWeek
+ , registered_last_month=RegisteredUsersLastMonth
+ , logged_last_hour=LoggedUsersLastHour
+ , logged_last_day=LoggedUsersLastDay
+ , logged_last_week=LoggedUsersLastWeek
+ , logged_last_month=LoggedUsersLastMonth
+ }
+ , group_stats=#group_stat_metrics{ count=GroupCount
+ , created_last_day=CreatedGroupsLastDay
+ , created_last_week=CreatedGroupsLastWeek
+ , created_last_month=CreatedGroupsLastMonth
+ }
+ , bridge_stats=#bridge_stat_metrics{ public_count=NumBridgesPublic
+ , private_count=NumBridgesPrivate
+ , connections=NumConnections
+ , unique_connections=NumUniqueConnections
+ , messages_on_flight=NumMessagesOnFlight
+ }
+ }) ->
+ #{ active_services => map_with_null_values(ServiceCounts)
+ , bot_count => map_with_null_values(BotCount)
+ , thread_count => map_with_null_values(ThreadCount)
+ , monitor_count => map_with_null_values(MonitorCount)
+ , service_count => map_with_null_values(ServiceCount)
+ , users => #{ count => UserCount
+ , registered_last_day => RegisteredUsersLastDay
+ , registered_last_week => RegisteredUsersLastWeek
+ , registered_last_month => RegisteredUsersLastMonth
+ , logged_last_hour => LoggedUsersLastHour
+ , logged_last_day => LoggedUsersLastDay
+ , logged_last_week => LoggedUsersLastWeek
+ , logged_last_month => LoggedUsersLastMonth
+ }
+ , groups => #{ count => GroupCount
+ , created_last_day => CreatedGroupsLastDay
+ , created_last_week => CreatedGroupsLastWeek
+ , created_last_month => CreatedGroupsLastMonth
+ }
+ , bridges => #{ public_count => NumBridgesPublic
+ , private_count => NumBridgesPrivate
+ , connections => NumConnections
+ , unique_connections => NumUniqueConnections
+ , messages_on_flight => NumMessagesOnFlight
+ }
+ }.
+
+serialize_errors(Errors) ->
+ lists:map(fun(Err) -> serialize_error(Err) end, Errors).
+
+serialize_error({Module, { ErrorNS, Error, _StackTrace }}) ->
+ binary:list_to_bin(lists:flatten(io_lib:format("Failed to get value from ~p (~p:~p)", [Module, ErrorNS, Error])));
+serialize_error({Module, Reason}) ->
+ binary:list_to_bin(lists:flatten(io_lib:format("Failed to get value from ~p: ~s", [Module, Reason]))).
+
+map_with_null_values(Orig) ->
+ maps:map(fun(_, V) ->
+ case V of
+ undefined -> null;
+ _ -> V
+ end
+ end, Orig).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_app.erl b/backend/apps/automate_rest_api/src/automate_rest_api_app.erl
index 5bca40d4..36daaee2 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_app.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_app.erl
@@ -8,15 +8,23 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
-start(_StartType, _StartArgs) ->
+start() ->
+ % Dependencies
+ {ok, _} = application:ensure_all_started(inets),
+ {ok, _} = application:ensure_all_started(cowboy),
+
+ %% Initialize process
automate_rest_api_sup:start_link().
+start(_StartType, _StartArgs) ->
+ start().
+
%%--------------------------------------------------------------------
stop(_State) ->
ok.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_autocomplete_user.erl b/backend/apps/automate_rest_api/src/automate_rest_api_autocomplete_user.erl
new file mode 100644
index 00000000..45454eb8
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_autocomplete_user.erl
@@ -0,0 +1,94 @@
+%%% @doc
+%%% REST endpoint to manager user name autocompletion.
+%%% @end
+
+-module(automate_rest_api_autocomplete_user).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , malformed_request/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { query :: binary() | undefined }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ Qs = cowboy_req:parse_qs(Req),
+ Query = proplists:get_value(<<"q">>, Qs),
+ {cowboy_rest, Req, #state{ query=Query }}.
+
+malformed_request(Req, State=#state{query=Query}) ->
+ case Query of
+ undefined -> % Query is required
+ {true, Req, State};
+ _ ->
+ {false, Req, State}
+ end.
+
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, ui) of
+ {true, _UserId} ->
+ { true, Req1, State };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{query=Query}) ->
+ case automate_storage:search_users(Query) of
+ { ok, Users } ->
+ Output = jiffy:encode(encode_user_list(Users)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+encode_user_list(Users) ->
+ #{ users => lists:map(fun encode_user/1, Users)
+ }.
+
+encode_user(#registered_user_entry{ id=Id
+ , username=Username
+ }) ->
+ #{ id => Id
+ , username => Username
+ }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_backend.erl b/backend/apps/automate_rest_api/src/automate_rest_api_backend.erl
index ac3441c3..03e3af86 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_backend.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_backend.erl
@@ -9,39 +9,37 @@
, login_user/1
, get_user/1
- , generate_token_for_user/1
- , is_valid_token/1
- , is_valid_token_uid/1
+ , is_valid_token/2
+ , is_valid_token_uid/2
, create_monitor/2
, lists_monitors_from_username/1
- , create_program/1
- , get_program/2
- , update_program_tags/3
- , update_program_status/3
- , get_program_tags/2
- , stop_program_threads/2
+ , create_program/3
+ , get_program/1
+ , get_program_logs/1
, lists_programs_from_username/1
, update_program/3
+ , update_program_by_id/2
, list_services_from_username/1
+ , get_services_metadata/2
, get_service_enable_how_to/2
, update_program_metadata/3
+ , update_program_metadata/2
, delete_program/2
+ , delete_program/1
, create_service_port/2
, list_custom_blocks_from_username/1
- , register_service/3
+ , register_service/4
, send_oauth_return/2
, list_bridges/1
- , delete_bridge/2
- , callback_bridge/3
+ , list_available_connections/1
+ , list_established_connections/1
, bridge_function_call/4
, create_custom_signal/2
- , list_custom_signals_from_user_id/1
, create_template/3
- , list_templates_from_user_id/1
, delete_template/2
, update_template/4
, get_template/2
@@ -49,21 +47,28 @@
%% Definitions
-include("./records.hrl").
--include("../../automate_service_registry/src/records.hrl").
-include("../../automate_storage/src/records.hrl").
+-include("../../automate_service_registry/src/records.hrl").
-include("../../automate_service_port_engine/src/records.hrl").
-include("../../automate_template_engine/src/records.hrl").
+-define(URLS, automate_rest_api_utils_urls).
+
%%====================================================================
%% API functions
%%====================================================================
-spec register_user(#registration_rec{}) -> {ok, continue | wait_for_mail_verification } | {error, _}.
-register_user(Reg) ->
- case automate_mail:is_enabled() of
+register_user(Reg=#registration_rec{ username=Username }) ->
+ case automate_storage_utils:validate_username(Username) of
false ->
- register_user_instantly(Reg);
+ {error, invalid_username};
true ->
- register_user_require_validation(Reg)
+ case automate_mail:is_enabled() of
+ false ->
+ register_user_instantly(Reg);
+ true ->
+ register_user_require_validation(Reg)
+ end
end.
verify_registration_with_code(RegistrationCode) ->
@@ -110,28 +115,27 @@ reset_password(VerificationCode, Password) ->
get_user(UserId) ->
automate_storage:get_user(UserId).
-generate_token_for_user(UserId) ->
- automate_storage:generate_token_for_user(UserId).
-
-is_valid_token(Token) when is_binary(Token) ->
- case automate_storage:get_session_username(Token, true) of
+-spec is_valid_token(binary(), session_scope_item()) -> {true, binary()} | false.
+is_valid_token(Token, Scope) when is_binary(Token) ->
+ case automate_storage:check_session_username(Token, Scope, true) of
{ ok, Username } ->
{true, Username};
{ error, session_not_found } ->
false;
{ error, Reason } ->
- io:format("Error getting session: ~p~n", [Reason]),
+ automate_logging:log_api(error, ?MODULE, {error_retrieving_session, Reason}),
false
end.
-is_valid_token_uid(Token) when is_binary(Token) ->
- case automate_storage:get_session_userid(Token, true) of
+-spec is_valid_token_uid(binary(), session_scope_item()) -> {true, binary()} | false.
+is_valid_token_uid(Token, Scope) when is_binary(Token) ->
+ case automate_storage:check_session_userid(Token, Scope, true) of
{ ok, UserId } ->
{true, UserId};
{ error, session_not_found } ->
false;
{ error, Reason } ->
- io:format("Error getting session: ~p~n", [Reason]),
+ automate_logging:log_api(error, ?MODULE, {error_retrieving_session, Reason}),
false
end.
@@ -141,7 +145,7 @@ create_monitor(Username, #monitor_descriptor{ type=Type, name=Name, value=Value
, name=Name
, value=Value
, id=none %% ID generated by the storage
- , user_id=none
+ , owner=none
}) of
{ ok, MonitorId } ->
{ ok, { MonitorId, Name } }
@@ -158,94 +162,114 @@ lists_monitors_from_username(Username) ->
|| {Id, Name} <- Monitors]}
end.
-create_program(Username) ->
- ProgramName = generate_program_name(),
- case automate_storage:create_program(Username, ProgramName) of
+create_program(Username, ProgramName, ProgramType) ->
+ case automate_storage:create_program(Username, ProgramName, ProgramType) of
{ ok, ProgramId } ->
{ ok, { ProgramId
, ProgramName
- , generate_url_for_program_name(Username, ProgramName) } }
+ , generate_url_for_program_name(Username, ProgramName)
+ , ProgramType
+ } }
end.
-get_program(Username, ProgramName) ->
- case automate_storage:get_program(Username, ProgramName) of
+get_program(ProgramId) ->
+ case automate_storage:get_program_from_id(ProgramId) of
{ok, ProgramData} ->
{ok, program_entry_to_program(ProgramData)};
X ->
X
end.
-update_program_tags(Username, ProgramName, Tags) ->
- case automate_storage:register_program_tags(ProgramName, Tags) of
- ok ->
- ok;
- { error, Reason } ->
- {error, Reason}
- end.
-
-update_program_status(Username, ProgramName, Status) ->
- case automate_bot_engine:change_program_status(Username, ProgramName, Status) of
- ok ->
- ok;
- { error, Reason } ->
- { error , Reason }
- end.
-
-get_program_tags(Username, ProgramId) ->
- case automate_storage:get_tags_program_from_id(ProgramId) of
- {ok, Tags} ->
- {ok, Tags};
+get_program_logs(ProgramId) ->
+ case automate_storage:get_logs_from_program_id(ProgramId) of
+ {ok, ErrorLogs} ->
+ case automate_storage:get_user_generated_logs(ProgramId) of
+ {ok, UserLogs} ->
+ {ok, ErrorLogs, UserLogs};
+ Y ->
+ Y
+ end;
X ->
X
end.
-stop_program_threads(UserId, ProgramId) ->
- case automate_bot_engine:stop_program_threads(UserId, ProgramId) of
- ok ->
- ok;
- { error, Reason } ->
- {error, Reason}
- end.
-
-spec lists_programs_from_username(binary()) -> {'ok', [ #program_metadata{} ] }.
lists_programs_from_username(Username) ->
case automate_storage:lists_programs_from_username(Username) of
{ok, Programs} ->
- {ok, [#program_metadata{ id=ProgramId
- , name=ProgramName
- , link=generate_url_for_program_name(Username, ProgramName)
- , enabled=Enabled
- }
- || {ProgramId, ProgramName, Enabled} <- Programs]}
+ {ok, lists:map(fun(#user_program_entry{ id=Id
+ , program_name=Name
+ , program_type=Type
+ , enabled=Enabled
+ , visibility=Visibility
+ }) ->
+ #program_metadata{ id=Id
+ , name=Name
+ , enabled=Enabled
+ , type=Type
+ , visibility=Visibility
+ }
+ end, Programs)}
end.
update_program(Username, ProgramName,
#program_content{ orig=Orig
, parsed=Parsed
- , type=Type }) ->
+ , type=Type
+ , pages=Pages
+ }) ->
case automate_storage:update_program(Username, ProgramName,
#stored_program_content{ orig=Orig
, parsed=Parsed
- , type=Type }) of
+ , type=Type
+ , pages=Pages
+ }) of
{ ok, ProgramId } ->
automate_bot_engine_launcher:update_program(ProgramId);
{ error, Reason } ->
{error, Reason}
end.
-update_program_metadata(Username, ProgramName,
- Metadata=#editable_user_program_metadata{program_name=NewProgramName}) ->
+update_program_by_id(ProgramId,
+ #program_content{ orig=Orig
+ , parsed=Parsed
+ , type=Type
+ , pages=Pages
+ }) ->
+
+ case automate_storage:update_program_by_id(ProgramId,
+ #stored_program_content{ orig=Orig
+ , parsed=Parsed
+ , type=Type
+ , pages=Pages
+ }) of
+ { ok, ProgramId } ->
+ automate_bot_engine_launcher:update_program(ProgramId);
+ { error, Reason } ->
+ {error, Reason}
+ end.
+update_program_metadata(Username, ProgramName, Metadata) ->
case automate_storage:update_program_metadata(Username,
ProgramName,
Metadata) of
- { ok, _ProgramId } ->
+ { ok, ProgramId } ->
+ {ok, #user_program_entry{ program_name=NewProgramName} } = automate_storage:get_program_from_id(ProgramId),
{ok, #{ <<"link">> => generate_url_for_program_name(Username, NewProgramName) }};
{ error, Reason } ->
{error, Reason}
end.
+-spec update_program_metadata(binary(), map()) -> ok | {error, binary()}.
+update_program_metadata(ProgramId, Metadata) ->
+ case automate_storage:update_program_metadata(ProgramId, Metadata) of
+ { ok, _ProgramId } ->
+ ok;
+ { error, Reason } ->
+ {error, Reason}
+ end.
+
-spec delete_program(binary(), binary()) -> ok | {error, any()}.
delete_program(Username, ProgramName) ->
@@ -257,47 +281,61 @@ delete_program(Username, ProgramName) ->
X
end.
+-spec delete_program(binary()) -> ok | {error, any()}.
+delete_program(ProgramId) ->
+ case automate_storage:delete_program(ProgramId) of
+ ok ->
+ {ok, _} = automate_bot_engine_launcher:stop_program(ProgramId),
+ ok;
+ X ->
+ X
+ end.
+
--spec list_services_from_username(binary()) -> {'ok', [ #service_metadata{} ]} | {error, term(), binary()}.
+-spec list_services_from_username(binary()) -> {'ok', [ #service_metadata{} ]}.
list_services_from_username(Username) ->
- {ok, UserId} = automate_storage:get_userid_from_username(Username),
- case automate_service_registry:get_all_services_for_user(UserId) of
+ {ok, Owner} = automate_storage:get_userid_from_username(Username),
+ case automate_service_registry:get_all_services_for_user(Owner) of
{ok, Services} ->
- {ok, get_services_metadata(Services, Username)};
- E = {error, _, _} ->
- E
+ {ok, get_services_metadata(Services, Owner)}
end.
+get_services_metadata(Services, Owner) ->
+ lists:filter(fun (V) ->
+ V =/= none
+ end,
+ lists:map(fun ({K, V}) -> get_service_metadata(K, V, Owner) end,
+ maps:to_list(Services))).
--spec get_service_enable_how_to(binary(), binary()) -> {ok, map() | none} | {error, not_found}.
+
+-spec get_service_enable_how_to(binary(), binary()) -> {ok, map() | none} | {error, not_found} | {error, no_connection} | {error, _}.
get_service_enable_how_to(Username, ServiceId) ->
case get_platform_service_how_to(Username, ServiceId) of
{ok, HowTo} ->
{ok, HowTo};
- {error, not_found} ->
- %% TODO: Implement user-defined services
- io:format("Error: non platform service required~n"),
- {error, not_found}
+ {error, Reason} ->
+ {error, Reason}
end.
--spec create_service_port(binary(), binary()) -> {ok, binary()}.
+-spec create_service_port(binary(), binary()) -> {ok, {binary(), binary()}}.
create_service_port(Username, ServicePortName) ->
- {ok, UserId} = automate_storage:get_userid_from_username(Username),
- {ok, ServicePortId } = automate_service_port_engine:create_service_port(UserId, ServicePortName),
- {ok, generate_url_for_service_port(UserId, ServicePortId)}.
+ {ok, Owner} = automate_storage:get_userid_from_username(Username),
+ {ok, ServicePortId } = automate_service_port_engine:create_service_port(Owner, ServicePortName),
+ {ok, {?URLS:bridge_control_url(ServicePortId), ServicePortId}}.
-spec list_custom_blocks_from_username(binary()) -> {ok, map()}.
list_custom_blocks_from_username(Username) ->
- {ok, UserId} = automate_storage:get_userid_from_username(Username),
- automate_service_port_engine:list_custom_blocks(UserId).
+ {ok, Owner} = automate_storage:get_userid_from_username(Username),
+ automate_service_port_engine:list_custom_blocks(Owner).
--spec register_service(binary(), binary(), map()) -> {ok, any} | {error, binary()}.
-register_service(Username, ServiceId, RegistrationData) ->
- {ok, UserId} = automate_storage:get_userid_from_username(Username),
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId, UserId),
- {ok, _} = automate_service_registry_query:send_registration_data(Module, UserId, RegistrationData).
+-spec register_service(binary(), binary(), map(), binary()) -> {ok, any} | {error, binary()}.
+register_service(Username, ServiceId, RegistrationData, ConnectionId) ->
+ {ok, Owner} = automate_storage:get_userid_from_username(Username),
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ {ok, _Result} = automate_service_registry_query:send_registration_data(Module, Owner, RegistrationData,
+ #{<<"connection_id">> => ConnectionId}).
-spec send_oauth_return(binary(), binary()) -> ok | {error, term()}.
send_oauth_return(ServicePortId, Qs) ->
@@ -310,49 +348,54 @@ send_oauth_return(ServicePortId, Qs) ->
-spec list_bridges(binary()) -> {ok, [#service_port_entry_extra{}]}.
list_bridges(Username) ->
- {ok, UserId} = automate_storage:get_userid_from_username(Username),
- {ok, _ServicePorts} = automate_service_port_engine:get_user_service_ports(UserId).
+ {ok, Owner} = automate_storage:get_userid_from_username(Username),
+ {ok, _ServicePorts} = automate_service_port_engine:get_user_service_ports(Owner).
--spec delete_bridge(binary(), binary()) -> ok | {error, binary()}.
-delete_bridge(UserId, BridgeId) ->
- automate_service_port_engine:delete_bridge(UserId, BridgeId).
+-spec list_available_connections(owner_id()) -> {ok, [{#service_port_entry{}, #service_port_configuration{}}]}.
+list_available_connections(Owner) ->
+ case automate_service_registry:get_all_services_for_user(Owner) of
+ {ok, Services} ->
+ EnabledServices = lists:filtermap(fun({ _ServiceId, #{ module := Module } }) ->
+ case automate_service_port_engine:is_module_connectable_bridge(Owner, Module) of
+ false -> false;
+ {false, _BridgeData} ->
+ false;
+ {true, BridgeData} ->
+ { true, BridgeData }
+ end
+ end, maps:to_list(Services)),
+ {ok, EnabledServices}
+ end.
-callback_bridge(UserId, BridgeId, Callback) ->
- automate_service_port_engine:callback_bridge(UserId, BridgeId, Callback).
+-spec list_established_connections(binary()) -> {ok, [#user_to_bridge_connection_entry{}]}.
+list_established_connections(UserId) ->
+ automate_service_port_engine:list_established_connections({user, UserId}).
--spec bridge_function_call(binary(), binary(), binary(), any()) -> {ok, map()} |{ error, term()}.
+-spec bridge_function_call(owner_id(), binary(), binary(), any()) -> {ok, map()} |{ error, term()}.
bridge_function_call(UserId, BridgeId, FunctionName, Arguments) ->
automate_service_port_engine:call_service_port(BridgeId, FunctionName, Arguments, UserId, #{}).
%% Custom signals
--spec create_custom_signal(binary(), binary()) -> {ok, binary()}.
-create_custom_signal(UserId, SignalName) ->
- automate_storage:create_custom_signal(UserId, SignalName).
-
--spec list_custom_signals_from_user_id(binary()) -> {ok, [#custom_signal_entry{}]}.
-list_custom_signals_from_user_id(UserId) ->
- automate_storage:list_custom_signals_from_user_id(UserId).
+-spec create_custom_signal(owner_id(), binary()) -> {ok, binary()}.
+create_custom_signal(Owner, SignalName) ->
+ automate_storage:create_custom_signal(Owner, SignalName).
%% Templates
--spec create_template(binary(), binary(), [any()]) -> {ok, binary()}.
-create_template(UserId, TemplateName, TemplateContent) ->
- automate_template_engine:create_template(UserId, TemplateName, TemplateContent).
+-spec create_template(owner_id(), binary(), [any()]) -> {ok, binary()}.
+create_template(Owner, TemplateName, TemplateContent) ->
+ automate_template_engine:create_template(Owner, TemplateName, TemplateContent).
--spec list_templates_from_user_id(binary()) -> {ok, [#template_entry{}]}.
-list_templates_from_user_id(UserId) ->
- automate_template_engine:list_templates_from_user_id(UserId).
+-spec delete_template(owner_id(), binary()) -> ok | {error, binary()}.
+delete_template(Owner, TemplateId) ->
+ automate_template_engine:delete_template(Owner, TemplateId).
--spec delete_template(binary(), binary()) -> ok | {error, binary()}.
-delete_template(UserId, TemplateId) ->
- automate_template_engine:delete_template(UserId, TemplateId).
+-spec update_template(owner_id(), binary(), binary(), [any()]) -> ok | {error, binary()}.
+update_template(Owner, TemplateId, TemplateName, TemplateContent) ->
+ automate_template_engine:update_template(Owner, TemplateId, TemplateName, TemplateContent).
--spec update_template(binary(), binary(), binary(), [any()]) -> ok | {error, binary()}.
-update_template(UserId, TemplateId, TemplateName, TemplateContent) ->
- automate_template_engine:update_template(UserId, TemplateId, TemplateName, TemplateContent).
-
--spec get_template(binary(), binary()) -> {ok, #template_entry{}} | {error, binary()}.
-get_template(UserId, TemplateId) ->
- automate_template_engine:get_template(UserId, TemplateId).
+-spec get_template(owner_id(), binary()) -> {ok, #template_entry{}} | {error, binary()}.
+get_template(Owner, TemplateId) ->
+ automate_template_engine:get_template(Owner, TemplateId).
%%====================================================================
%% Internal functions
@@ -377,8 +420,7 @@ register_user_require_validation(#registration_rec{ email=Email
case automate_storage:create_mail_verification_entry(UserId) of
{ok, MailVerificationCode} ->
case automate_mail:send_registration_verification(Username, Email, MailVerificationCode) of
- { ok, Url } ->
- io:format("Url: ~p~n", [Url]),
+ { ok, _Url } ->
{ ok, wait_for_mail_verification };
{error, Reason} ->
automate_storage:delete_user(UserId),
@@ -392,70 +434,58 @@ register_user_require_validation(#registration_rec{ email=Email
{ error, Reason }
end.
-get_services_metadata(Services, Username) ->
- lists:filter(fun (V) ->
- V =/= none
- end,
- lists:map(fun ({K, V}) -> get_service_metadata(K, V, Username) end,
- maps:to_list(Services))).
-
get_service_metadata(Id
, #{ name := Name
, description := _Description
, module := Module
}
- , Username) ->
- try automate_service_registry_query:is_enabled_for_user(Module, Username) of
+ , Owner) ->
+ try automate_service_registry_query:is_enabled_for_user(Module, Owner) of
{ok, Enabled} ->
#service_metadata{ id=Id
, name=Name
- , link=generate_url_for_service_id(Username, Id)
+ , link=?URLS:service_id_url(Id)
, enabled=Enabled
}
catch X:Y ->
- io:fwrite("Error getting service metadata ~p:~p~n", [X, Y]),
+ automate_logging:log_api(error, ?MODULE, io_lib:format("Error getting service metadata ~p:~p", [X, Y])),
none
end.
-generate_url_for_service_id(Username, ServiceId) ->
- binary:list_to_bin(lists:flatten(io_lib:format("/api/v0/users/~s/services/id/~s", [Username, ServiceId]))).
-
-%% *TODO* generate more interesting names.
-generate_program_name() ->
- binary:list_to_bin(uuid:to_string(uuid:uuid4())).
-
generate_url_for_program_name(Username, ProgramName) ->
binary:list_to_bin(lists:flatten(io_lib:format("/api/v0/users/~s/programs/~s", [Username, ProgramName]))).
generate_url_for_monitor_name(Username, MonitorName) ->
binary:list_to_bin(lists:flatten(io_lib:format("/api/v0/users/~s/monitors/~s", [Username, MonitorName]))).
-generate_url_for_service_port(UserId, ServicePortId) ->
- binary:list_to_bin(lists:flatten(io_lib:format("/api/v0/users/id/~s/bridges/id/~s/communication", [UserId, ServicePortId]))).
-
-
program_entry_to_program(#user_program_entry{ id=Id
- , user_id=UserId
+ , owner=Owner
, program_name=ProgramName
, program_type=ProgramType
, program_parsed=ProgramParsed
, program_orig=ProgramOrig
- , enabled=_Enabled
+ , enabled=Enabled
+ , last_upload_time=LastUploadTime
+ , visibility=Visibility
}) ->
+ {OwnerType, OwnerId} = Owner,
#user_program{ id=Id
- , user_id=UserId
+ , owner=#{ type => OwnerType, id => OwnerId }
, program_name=ProgramName
, program_type=ProgramType
, program_parsed=ProgramParsed
, program_orig=ProgramOrig
+ , enabled=Enabled
+ , last_upload_time=LastUploadTime
+ , visibility=Visibility
}.
--spec get_platform_service_how_to(binary(), binary()) -> {ok, map() | none} | {error, not_found}.
+-spec get_platform_service_how_to(binary(), binary()) -> {ok, map() | none} | {error, not_found} | {error, no_connection} | {error, _}.
get_platform_service_how_to(Username, ServiceId) ->
- {ok, UserId} = automate_storage:get_userid_from_username(Username),
- case automate_service_registry:get_service_by_id(ServiceId, UserId) of
+ {ok, Owner} = automate_storage:get_userid_from_username(Username),
+ case automate_service_registry:get_service_by_id(ServiceId) of
E = {error, not_found} ->
E;
{ok, #{ module := Module }} ->
- automate_service_registry_query:get_how_to_enable(Module, #{ user_id => UserId, user_name => Username})
+ automate_service_registry_query:get_how_to_enable(Module, Owner)
end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_callback.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_callback.erl
index 17a2ab14..3582dadb 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_callback.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_callback.erl
@@ -13,10 +13,15 @@
-export([ to_json/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
-include("../../automate_service_port_engine/src/records.hrl").
--record(state, { user_id, bridge_id, callback }).
+-record(state, { user_id :: binary()
+ , bridge_id :: binary()
+ , callback :: binary()
+ , sequence_id :: binary() | undefined
+ }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -24,10 +29,15 @@ init(Req, _Opts) ->
BridgeId = cowboy_req:binding(bridge_id, Req),
Callback = cowboy_req:binding(callback, Req),
Req1 = automate_rest_api_cors:set_headers(Req),
+
+ Qs = cowboy_req:parse_qs(Req1),
+ SequenceId = proplists:get_value(<<"sequence_id">>, Qs),
+
{cowboy_rest, Req1
, #state{ user_id=UserId
, bridge_id=BridgeId
, callback=Callback
+ , sequence_id=SequenceId
}}.
%% CORS
@@ -37,10 +47,9 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[Bridge callback] Asking for methods~n", []),
{[<<"GET">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{ bridge_id=BridgeId }) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
@@ -52,11 +61,10 @@ is_authorized(Req, State) ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
#state{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, { call_bridge_callback, BridgeId }) of
{true, UserId} ->
{ true, Req1, State };
- {true, TokenUserId} -> %% Non matching user_id
- io:fwrite("Url UID: ~p | Token UID: ~p~n", [UserId, TokenUserId]),
+ {true, _TokenUserId} -> %% Non matching user_id
{ { false, <<"Unauthorized to create a program here">>}, Req1, State };
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
@@ -66,27 +74,24 @@ is_authorized(Req, State) ->
%% GET handler
content_types_provided(Req, State) ->
- io:fwrite("Bridge callback: ~p~n", [State]),
{[{{<<"application">>, <<"json">>, []}, to_json}],
Req, State}.
-to_json(Req, State) ->
- #state{bridge_id=BridgeId, callback=Callback, user_id=UserId} = State,
- case automate_rest_api_backend:callback_bridge(UserId, BridgeId, Callback) of
+to_json(Req, State=#state{bridge_id=BridgeId, callback=Callback, user_id=UserId, sequence_id=SequenceId}) ->
+ case automate_service_port_engine:callback_bridge({user, UserId}, BridgeId, Callback, SequenceId) of
{ok, Result} ->
Output = jiffy:encode(Result),
+ Res = ?UTILS:send_json_format(Req),
- Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
- Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
-
- { Output, Res2, State };
+ { Output, Res, State };
{error, Reason} ->
Code = case Reason of
not_found -> 404;
unauthorized -> 403;
+ no_connection -> 409; %% Conflict
_ -> 500
end,
Output = jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }),
- cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req)
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
end.
-
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_function_specific.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_function_specific.erl
index 4a8e6f50..9add0fc8 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_function_specific.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_function_specific.erl
@@ -14,13 +14,15 @@
-export([ accept_function_call/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
-include("../../automate_service_port_engine/src/records.hrl").
--record(state, { user_id, bridge_id, function_name }).
+-record(state, { user_id :: binary(), bridge_id :: binary(), function_name :: binary(), metrics_data :: any() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
+ MetricsData = ?UTILS:start_metrics(Req, api_function_call),
UserId = cowboy_req:binding(user_id, Req),
BridgeId = cowboy_req:binding(bridge_id, Req),
FunctionName = cowboy_req:binding(function, Req),
@@ -29,6 +31,7 @@ init(Req, _Opts) ->
, #state{ user_id=UserId
, bridge_id=BridgeId
, function_name=FunctionName
+ , metrics_data=MetricsData
}}.
resource_exists(Req, State) ->
@@ -46,10 +49,9 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[Bridge function call] Asking for methods~n", []),
{[<<"POST">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{bridge_id=BridgeId, function_name=FunctionName}) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
@@ -61,10 +63,10 @@ is_authorized(Req, State) ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
#state{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, {call_bridge, BridgeId, FunctionName}) of
{true, UserId} ->
{ true, Req1, State };
- {true, TokenUserId} -> %% Non matching user_id
+ {true, _TokenUserId} -> %% Non matching user_id
{ { false, <<"Unauthorized to create a program here">>}, Req1, State };
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
@@ -74,16 +76,15 @@ is_authorized(Req, State) ->
%% POST handler
content_types_accepted(Req, State) ->
- io:fwrite("Bridge function call: ~p~n", [State]),
{[{{<<"application">>, <<"json">>, []}, accept_function_call}],
Req, State}.
accept_function_call(Req, State) ->
- #state{bridge_id=BridgeId, function_name=FunctionName, user_id=UserId} = State,
- {ok, Body, _} = read_body(Req),
+ #state{bridge_id=BridgeId, function_name=FunctionName, user_id=UserId, metrics_data=MetricsData} = State,
+ {ok, Body, _} = ?UTILS:read_body(Req),
#{<<"arguments">> := Arguments } = jiffy:decode(Body, [return_maps]),
- case automate_rest_api_backend:bridge_function_call(UserId, BridgeId, FunctionName, Arguments) of
+ case automate_rest_api_backend:bridge_function_call({user, UserId}, BridgeId, FunctionName, Arguments) of
{ok, Result } ->
Output = encode_result(Result),
@@ -91,6 +92,7 @@ accept_function_call(Req, State) ->
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
+ ?UTILS:end_metrics(MetricsData),
{ true, Res3, State };
{error, Reason} ->
Code = case Reason of
@@ -99,7 +101,9 @@ accept_function_call(Req, State) ->
_ -> 500
end,
Output = jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }),
- cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req)
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ ?UTILS:end_metrics_with_error(MetricsData, Reason),
+ {stop, Res, State}
end.
%% Helper functions
@@ -111,12 +115,3 @@ encode_result(#{ <<"success">> := true, <<"result">> := Result }) ->
encode_result(Result) ->
%% Not a positive result, just encode the returned data to help debugging.
jiffy:encode(Result).
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_resources_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_resources_root.erl
new file mode 100644
index 00000000..3ae7033b
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_resources_root.erl
@@ -0,0 +1,117 @@
+%%% @doc
+%%% REST endpoint to manage bridge.
+%%% @end
+
+-module(automate_rest_api_bridge_resources_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { bridge_id :: binary()
+ , owner :: owner_id() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ BridgeId = cowboy_req:binding(bridge_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ bridge_id=BridgeId
+ , owner=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{ bridge_id=BridgeId }) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { list_bridge_resources, BridgeId }) of
+ {true, UserId} ->
+ { true, Req1, State#state{ owner={user, UserId} } };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+to_json(Req, State=#state{ bridge_id=BridgeId, owner=Owner }) ->
+ case automate_service_port_engine:get_bridge_configuration(BridgeId) of
+ {ok, #service_port_configuration{resources=Resources}} ->
+ case automate_service_port_engine:list_established_connections(Owner, BridgeId) of
+ {ok, Results} ->
+ ResourceList = merge_to_map(lists:flatmap(
+ fun(#user_to_bridge_connection_entry{id=ConnectionId}) ->
+ {ok, ConnectionShares} = automate_service_port_engine:get_connection_shares(ConnectionId),
+
+ lists:map(fun(ResourceName) ->
+ {ok, #{ <<"result">> := Values }} = automate_service_port_engine:callback_bridge_through_connection(ConnectionId, BridgeId, ResourceName, undefined),
+ {ResourceName, maps:map(fun(K, V) -> V#{ connection_id => ConnectionId
+ , shared_with => find_shares(ConnectionShares, ResourceName, K)
+ } end, Values)}
+ end, Resources)
+ end, Results)),
+ Res = ?UTILS:send_json_format(Req),
+ { jiffy:encode(ResourceList), Res, State }
+ end;
+ {error, Reason} ->
+ Code = case Reason of
+ not_found -> 404
+ end,
+ Output = jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }),
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
+
+ end.
+
+merge_to_map(List) ->
+ merge_to_map(List, #{}).
+
+merge_to_map([], Acc) ->
+ Acc;
+merge_to_map([{K, V} | T], Acc) ->
+ case Acc of
+ #{ K := Prev } ->
+ merge_to_map(T, Acc#{ K => Prev#{ K =>V } });
+ _ ->
+ merge_to_map(T, Acc#{ K => V } )
+ end.
+
+
+find_shares(ConnectionShares, ResourceName, Value) ->
+ case ConnectionShares of
+ #{ ResourceName := #{ Value := Result } } ->
+ lists:map(fun({Type, Id}) -> #{ type => Type, id => Id } end, Result);
+ _ ->
+ []
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_history.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_history.erl
new file mode 100644
index 00000000..10bc0d98
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_history.erl
@@ -0,0 +1,94 @@
+%%% @doc
+%%% REST endpoint to manage bridge signal history.
+%%% @end
+
+-module(automate_rest_api_bridge_signal_history).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { bridge_id :: binary()
+ , group_id :: binary() | undefined
+ , owner :: owner_id() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ BridgeId = cowboy_req:binding(bridge_id, Req),
+ Qs = cowboy_req:parse_qs(Req),
+ GroupId = proplists:get_value(<<"group_id">>, Qs),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ bridge_id=BridgeId
+ , group_id=GroupId
+ , owner=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{ group_id=GroupId, bridge_id=BridgeId }) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { read_bridge_signal, BridgeId }) of
+ {true, UserId} ->
+ case GroupId of
+ undefined ->
+ { true, Req1, State#state{ owner={user, UserId} } };
+ GId when is_binary(GId) ->
+ case automate_storage:is_allowed_to_write_in_group({user, UserId}, GroupId) of
+ true ->
+ { true, Req1, State#state{ owner={group, GroupId} } };
+ false ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% Route by Method
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+
+%% GET handler
+to_json(Req, State=#state{bridge_id=BridgeId, owner=Owner}) ->
+ case automate_logging:get_signal_by_bridge_and_owner_history(BridgeId, Owner) of
+ {ok, Data} ->
+ Req1 = ?UTILS:send_json_format(Req),
+ %% Insert the data inside a iolist.
+ %% This is to avoid json-encoding and decoding a potentially big JSON blob.
+ { [<<"{ \"success\": true, \"data\": ">>, Data, <<"}">>], Req1, State };
+ { error, Reason } ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ success => false, message => Reason }), Req),
+ { false, Req1, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_root.erl
index 8df8ed78..8d94a4f3 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_root.erl
@@ -8,42 +8,59 @@
-export([websocket_handle/2]).
-export([websocket_info/2]).
--record(state, { user_id :: binary()
+-define(PING_INTERVAL_MILLISECONDS, 15000).
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { owner :: owner_id()
, bridge_id :: binary()
, authorized :: boolean()
, errorCode :: binary() | none
}).
init(Req, _Opts) ->
- UserId = cowboy_req:binding(user_id, Req),
BridgeId = cowboy_req:binding(bridge_id, Req),
- {IsAuthorized, ErrorCode} = check_is_authorized(Req, UserId),
+ {IsAuthorized, ErrorCode, Owner} = check_is_authorized(Req, BridgeId),
{cowboy_websocket, Req, #state{ bridge_id=BridgeId
- , user_id=UserId
+ , owner=Owner
, authorized=IsAuthorized
, errorCode=ErrorCode
}}.
-check_is_authorized(Req, UserId) ->
- case cowboy_req:header(<<"authorization">>, Req, undefined) of
+check_is_authorized(Req, BridgeId) ->
+ Qs = cowboy_req:parse_qs(Req),
+ Token = case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ proplists:get_value(<<"token">>, Qs, undefined);
+ Tok -> Tok
+ end,
+
+
+ case Token of
undefined ->
- { false, <<"Authorization header not found">> };
+ { false, <<"Authorization data not found">>, undefined };
X ->
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, { read_bridge_signal, BridgeId }) of
{true, UserId} ->
- { true, none };
- {true, TokenUserId} -> %% Non matching user_id
- io:fwrite("Url UID: ~p | Token UID: ~p~n", [UserId, TokenUserId]),
- { false, <<"Unauthorized to connect here">> };
+ case proplists:get_value(<<"as_group">>, Qs, undefined) of
+ undefined ->
+ { true, none, {user, UserId} };
+ GroupId ->
+ case automate_storage:can_user_edit_as({user, UserId}, {group, GroupId}) of
+ true ->
+ { true, none, {group, GroupId} };
+ false ->
+ { false, <<"Unauthorized operation">>, undefined }
+ end
+ end;
false ->
- { false, <<"Authorization not correct">> }
+ { false, <<"Authorization not correct">>, undefined }
end
end.
websocket_init(State=#state{ bridge_id=BridgeId
- , user_id=UserId
+ , owner=Owner
, authorized=IsAuthorized
, errorCode=ErrorCode
}) ->
@@ -51,8 +68,9 @@ websocket_init(State=#state{ bridge_id=BridgeId
false ->
{ reply, { close, ErrorCode }, State };
true ->
- case automate_service_port_engine:listen_bridge(BridgeId, UserId) of
+ case automate_service_port_engine:listen_bridge(BridgeId, Owner) of
ok ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
{ok, State};
{error, Error} ->
{ reply, { close, io_lib:format("Error: ~p", [Error]) }, State }
@@ -63,10 +81,15 @@ websocket_handle(_Message, State) ->
%% Ignore everything
{ok, State}.
+
+websocket_info(ping_interval, State) ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {reply, ping, State};
+
websocket_info({channel_engine, _From, Data }, State) ->
Serialized = jiffy:encode(Data),
{reply, {binary, Serialized}, State};
websocket_info(Message, State) ->
- io:fwrite("Unexpected message: ~p~n", [Message]),
+ automate_logging:log_api(warning, ?MODULE, {unexpected_message, Message}),
{reply, {binary, Message}, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_specific.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_specific.erl
new file mode 100644
index 00000000..c26524cc
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_signal_specific.erl
@@ -0,0 +1,90 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_bridge_signal_specific).
+-export([ init/2 ]).
+-export([websocket_init/1]).
+-export([websocket_handle/2]).
+-export([websocket_info/2]).
+
+-define(PING_INTERVAL_MILLISECONDS, 15000).
+
+-record(state, { user_id :: binary()
+ , bridge_id :: binary()
+ , key :: binary()
+ , authorized :: boolean()
+ , errorCode :: binary() | none
+ }).
+
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ BridgeId = cowboy_req:binding(bridge_id, Req),
+ Key = cowboy_req:binding(key, Req),
+ {IsAuthorized, ErrorCode} = check_is_authorized(Req, UserId, BridgeId, Key),
+
+ {cowboy_websocket, Req, #state{ user_id=UserId
+ , bridge_id=BridgeId
+ , key=Key
+ , authorized=IsAuthorized
+ , errorCode=ErrorCode
+ }}.
+
+check_is_authorized(Req, UserId, BridgeId, Key) ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { false, <<"Authorization header not found">> } ;
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {read_bridge_signal, BridgeId, Key }) of
+ {true, UserId} ->
+ { true, none };
+ {true, TokenUserId} -> %% Non matching user_id
+ automate_logging:log_api(warning, ?MODULE,
+ io_lib:format("[WS/SignalSpecific] Url UID: ~p | Token UID: ~p~n", [UserId, TokenUserId])),
+ { false, <<"Unauthorized to connect here">> };
+ false ->
+ { false, <<"Authorization not correct">> }
+ end
+ end.
+
+websocket_init(State=#state{ bridge_id=BridgeId
+ , user_id=UserId
+ , key=Key
+ , authorized=IsAuthorized
+ , errorCode=ErrorCode
+ }) ->
+ case IsAuthorized of
+ false ->
+ { reply, { close, ErrorCode }, State };
+ true ->
+ case automate_service_port_engine:listen_bridge(BridgeId, {user, UserId}, {Key}) of
+ ok ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {ok, State};
+ {error, Error} ->
+ { reply, { close, io_lib:format("Error: ~p", [Error]) }, State }
+ end
+ end.
+
+websocket_handle(_Message, State) ->
+ %% Ignore everything
+ {ok, State}.
+
+
+websocket_info(ping_interval, State) ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {reply, ping, State};
+
+websocket_info({channel_engine, _From, Data=#{ <<"key">> := Key } },
+ State=#state{ key=Key }) ->
+ Serialized = jiffy:encode(Data),
+ {reply, {binary, Serialized}, State};
+
+websocket_info({channel_engine, _From, #{ <<"key">> := _AnotherKey } },
+ State=#state{ key=_Key }) ->
+ %% TODO: This can be used to test that only relevant data is sent
+ {ok, State};
+
+websocket_info(Message, State) ->
+ automate_logging:log_api(warning, ?MODULE, {unexpected_message, Message}),
+ {reply, {binary, Message}, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_tokens_by_name_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_tokens_by_name_root.erl
new file mode 100644
index 00000000..eb683314
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_tokens_by_name_root.erl
@@ -0,0 +1,81 @@
+%%% @doc
+%%% REST endpoint to manage bridge.
+%%% @end
+
+-module(automate_rest_api_bridge_tokens_by_name_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , delete_resource/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { bridge_id :: binary()
+ , owner :: owner_id() | undefined
+ , group_id :: binary() | undefined
+ , token_name :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ BridgeId = cowboy_req:binding(bridge_id, Req),
+ TokenName = cowboy_req:binding(token_name, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ Qs = cowboy_req:parse_qs(Req),
+ GroupId = proplists:get_value(<<"group_id">>, Qs),
+ {cowboy_rest, Req1
+ , #state{ bridge_id=BridgeId
+ , token_name=TokenName
+ , owner=undefined
+ , group_id=GroupId
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"DELETE">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{ bridge_id=BridgeId }) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { delete_bridge_tokens, BridgeId }) of
+ {true, UserId} ->
+ {ok, Owner} = automate_service_port_engine:get_bridge_owner(BridgeId),
+ case automate_storage:can_user_admin_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% DELETE handler
+delete_resource(Req, State=#state{ bridge_id=BridgeId, token_name=TokenName }) ->
+ case automate_service_port_engine:delete_bridge_token_by_name(BridgeId, TokenName) of
+ ok ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
+ { true, Req1, State };
+ {error, not_found} ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, debug => not_found}), Req),
+ { false, Req1, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_bridge_tokens_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_tokens_root.erl
new file mode 100644
index 00000000..58fb3e28
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_bridge_tokens_root.erl
@@ -0,0 +1,147 @@
+%%% @doc
+%%% REST endpoint to manage bridge.
+%%% @end
+
+-module(automate_rest_api_bridge_tokens_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , content_types_accepted/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ , accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(URLS, automate_rest_api_utils_urls).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { bridge_id :: binary()
+ , owner :: owner_id() | undefined
+ , group_id :: binary() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ BridgeId = cowboy_req:binding(bridge_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ Qs = cowboy_req:parse_qs(Req),
+ GroupId = proplists:get_value(<<"group_id">>, Qs),
+ {cowboy_rest, Req1
+ , #state{ bridge_id=BridgeId
+ , owner=undefined
+ , group_id=GroupId
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{ bridge_id=BridgeId }) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ Method ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ Scope = case Method of
+ <<"GET">> -> {list_bridge_tokens, BridgeId};
+ <<"POST">> -> {create_bridge_tokens, BridgeId}
+ end,
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UserId} ->
+ {ok, Owner} = automate_service_port_engine:get_bridge_owner(BridgeId),
+ case automate_storage:can_user_admin_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+to_json(Req, State=#state{ bridge_id=BridgeId }) ->
+ case automate_service_port_engine:list_bridge_tokens(BridgeId) of
+ {ok, Tokens} ->
+ Data = lists:map(fun(#bridge_token_entry{token_name=Name}) ->
+ #{ name => Name }
+ end, Tokens),
+ Output = jiffy:encode(
+ #{ success => true
+ , tokens => Data
+ }),
+
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []},
+ accept_json}],
+ Req, State}.
+
+-spec accept_json(cowboy_req:req(), #state{}) -> {{true, iolist()}, cowboy_req:req(), #state{}}.
+accept_json(Req, State=#state{owner=Owner, bridge_id=BridgeId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ #{ <<"name">> := TokenName
+ } = Data = jiffy:decode(Body, [return_maps]),
+ ExpiresOn = case Data of
+ #{ <<"expires_in">> := _ExpiresOn } ->
+ undefined;
+ _ ->
+ undefined
+ end,
+
+ case automate_service_port_engine:create_bridge_token(BridgeId, Owner, TokenName, ExpiresOn) of
+ {ok, TokenKey} ->
+ Output = jiffy:encode(#{ name => TokenName
+ , key => TokenKey
+ }),
+ Res2 = ?UTILS:send_json_output(Output, Req1),
+
+ { {true, ?URLS:bridge_token_by_name_url(BridgeId, TokenName)}, Res2, State};
+ {error, name_taken} ->
+ Output = jiffy:encode(
+ #{ success => false
+ , error => name_taken
+ }),
+
+ ConflictStatusCode = 409,
+
+ Res = cowboy_req:reply(ConflictStatusCode, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
+
+ end.
+
+%% Declare resources being created as not existing yet in this endpoint.
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ {false, Req, State};
+ _ ->
+ { true, Req, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_connection_by_id.erl b/backend/apps/automate_rest_api/src/automate_rest_api_connection_by_id.erl
new file mode 100644
index 00000000..818ced45
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_connection_by_id.erl
@@ -0,0 +1,96 @@
+%%% @doc
+%%% REST endpoint to manage a specific connection.
+%%% @end
+
+-module(automate_rest_api_connection_by_id).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ ]).
+
+-export([ accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { connection_id :: binary()
+ , owner :: owner_id() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ConnectionId = cowboy_req:binding(connection_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ connection_id=ConnectionId
+ , owner=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"PATCH">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{connection_id=ConnectionId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { edit_connection, ConnectionId }) of
+ {true, UserId} ->
+ {ok, Owner} = automate_service_port_engine:get_connection_owner(ConnectionId),
+ case automate_storage:can_user_edit_as({user, UserId}, Owner) of
+ true ->
+ { true, Req1, State#state{ owner={user, UserId} } };
+ false ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+
+
+%% Route by Method
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+accept_json(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"PATCH">> ->
+ accept_json_patch(Req, State)
+ end.
+
+%% PATCH handler
+accept_json_patch(Req, State=#state{connection_id=ConnectionId, owner=Owner}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
+ case Parsed of
+ #{ <<"save_signals">> := SaveSignals } ->
+ case automate_service_port_engine:set_save_signals_on_connection(ConnectionId, Owner, SaveSignals) of
+ ok ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true }), Req),
+ { true, Req2, State };
+ { error, Reason } ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req1),
+ { false, Req2, State }
+ end
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_connection_resource_by_name_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_connection_resource_by_name_root.erl
new file mode 100644
index 00000000..a3a3c4f0
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_connection_resource_by_name_root.erl
@@ -0,0 +1,91 @@
+%%% @doc
+%%% REST endpoint to manage connection resources.
+%%% @end
+
+-module(automate_rest_api_connection_resource_by_name_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ ]).
+
+-export([ accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { connection_id :: binary()
+ , owner :: owner_id() | undefined
+ , resource_name :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ConnectionId = cowboy_req:binding(connection_id, Req),
+ ResourceName = cowboy_req:binding(resource_name, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ connection_id=ConnectionId
+ , owner=undefined
+ , resource_name=ResourceName
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"PATCH">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{connection_id=ConnectionId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { edit_connection_shares, ConnectionId }) of
+ {true, UserId} ->
+ case automate_service_port_engine:get_connection_owner(ConnectionId) of
+ {ok, {user, UserId}} ->
+ { true, Req1, State#state{ owner={user, UserId} } };
+ {ok, _ } ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+
+
+%% PATCH handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+accept_json(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"PATCH">> ->
+ patch_json(Req, State)
+ end.
+
+patch_json(Req, State=#state{ connection_id=ConnectionId, resource_name=ResourceName }) ->
+ {ok, Body, _} = ?UTILS:read_body(Req),
+ Data = jiffy:decode(Body, [return_maps]),
+ case Data of
+ #{ <<"shared">> := Shares } when is_map(Shares) ->
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId, ResourceName, Shares)
+ end,
+ {true, Req, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_connections_available_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_connections_available_root.erl
new file mode 100644
index 00000000..568d4e05
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_connections_available_root.erl
@@ -0,0 +1,100 @@
+%%% @doc
+%%% REST endpoint to get available connection points
+%%% @end
+
+-module(automate_rest_api_connections_available_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+
+-record(state, { user_id :: binary() }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ {cowboy_rest, Req
+ , #state{ user_id=UserId }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ #state{user_id=UserId} = State,
+ case automate_rest_api_backend:is_valid_token_uid(X, list_connections_available) of
+ {true, UserId} ->
+ { true, Req1, State };
+ {true, _} -> %% Non matching user_id
+ { { false, <<"Unauthorized here">>}, Req1, State };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State) ->
+ #state{user_id=UserId} = State,
+ case automate_rest_api_backend:list_available_connections({user, UserId}) of
+ { ok, Connections } ->
+
+ Output = jiffy:encode(lists:map(fun to_map/1, Connections)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+to_map({#service_port_entry{ id=Id
+ , name=Name
+ , owner={OwnerType, OwnerId}
+ }
+ , #service_port_configuration{ service_id=ServiceId }
+ }) ->
+ #{ id => Id
+ , name => Name
+ , owner => OwnerId
+ , owner_full => #{ type => OwnerType, id => OwnerId }
+ , service_id => ServiceId
+ }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_connections_established_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_connections_established_root.erl
new file mode 100644
index 00000000..34073c5b
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_connections_established_root.erl
@@ -0,0 +1,88 @@
+%%% @doc
+%%% REST endpoint to get available connection points
+%%% @end
+
+-module(automate_rest_api_connections_established_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { user_id :: binary() }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ {cowboy_rest, Req
+ , #state{ user_id=UserId }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ #state{user_id=UserId} = State,
+ case automate_rest_api_backend:is_valid_token_uid(X, list_connections_established) of
+ {true, UserId} ->
+ { true, Req1, State };
+ {true, _} -> %% Non matching user_id
+ { { false, <<"Unauthorized here">>}, Req1, State };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State) ->
+ #state{user_id=UserId} = State,
+ case automate_rest_api_backend:list_established_connections(UserId) of
+ { ok, Connections } ->
+
+ Output = jiffy:encode(lists:filtermap(fun ?FORMATTING:connection_to_json/1, Connections)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_connections_pending_wait.erl b/backend/apps/automate_rest_api/src/automate_rest_api_connections_pending_wait.erl
new file mode 100644
index 00000000..1cc4326f
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_connections_pending_wait.erl
@@ -0,0 +1,56 @@
+%%% @doc
+%%% WebSocket endpoint to listen to completion on a pending connection.
+%%% @end
+
+-module(automate_rest_api_connections_pending_wait).
+-export([init/2]).
+-export([websocket_init/1]).
+-export([websocket_handle/2]).
+-export([websocket_info/2]).
+
+
+-define(PING_INTERVAL_MILLISECONDS, 15000).
+-include("../../automate_service_port_engine/src/records.hrl").
+
+-record(state, { user_id :: binary()
+ , connection_id :: binary()
+ }).
+
+
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ ConnectionId = cowboy_req:binding(connection_id, Req),
+
+ {cowboy_websocket, Req, #state{ connection_id=ConnectionId
+ , user_id=UserId
+ }}.
+
+websocket_init(State=#state{ connection_id=ConnectionId
+ }) ->
+
+ {ok, #user_to_bridge_pending_connection_entry{ channel_id=ChannelId }} = automate_service_port_engine:get_pending_connection_info(ConnectionId),
+
+ ok = automate_channel_engine:listen_channel(ChannelId),
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+
+ {ok, State}.
+
+websocket_handle(pong, State) ->
+ {ok, State};
+websocket_handle(Message, State) ->
+ automate_logging:log_api(warning, ?MODULE, {unexpected_message, Message, websocket}),
+ {ok, State}.
+
+
+websocket_info(ping_interval, State) ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {reply, ping, State};
+
+websocket_info({channel_engine, _ChannelId, connection_established}, State) ->
+ {reply, [ { text, jiffy:encode(#{ success => true, type => connection_established }) }
+ , { close, 1000, <<"Wait completed">> }
+ ], State};
+
+websocket_info(Message, State) ->
+ automate_logging:log_api(warning, ?MODULE, {unexpected_message, Message, internal}),
+ {ok, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_custom_blocks_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_custom_blocks_root.erl
index 62fd0003..4efb47ec 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_custom_blocks_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_custom_blocks_root.erl
@@ -16,7 +16,8 @@
-include("./records.hrl").
-include("../../automate_service_port_engine/src/records.hrl").
--record(state, { username }).
+-record(state, { username :: binary() }).
+-define(FORMATTING, automate_rest_api_utils_formatting).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -32,7 +33,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"GET">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -47,7 +47,7 @@ is_authorized(Req, State) ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
#state{username=Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ case automate_rest_api_backend:is_valid_token(X, list_custom_blocks) of
{true, Username} ->
{ true, Req1, State };
{true, _} -> %% Non matching username
@@ -65,11 +65,9 @@ content_types_provided(Req, State) ->
-spec to_json(cowboy_req:req(), #state{})
-> {binary(),cowboy_req:req(), #state{}}.
-to_json(Req, State) ->
- #state{username=Username} = State,
+to_json(Req, State=#state{username=Username}) ->
case automate_rest_api_backend:list_custom_blocks_from_username(Username) of
{ ok, CustomBlocks } ->
-
Output = jiffy:encode(maps:map(fun encode_blocks/2, CustomBlocks)),
Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
@@ -87,6 +85,7 @@ encode_block(#service_port_block{ block_id=BlockId
, block_type=BlockType
, block_result_type=BlockResultType
, save_to=SaveTo
+ , show_in_toolbox=ShowInToolbox
}) ->
#{ <<"block_id">> => BlockId
, <<"function_name">> => FunctionName
@@ -94,7 +93,8 @@ encode_block(#service_port_block{ block_id=BlockId
, <<"arguments">> => lists:map(fun encode_argument/1, Arguments)
, <<"block_type">> => BlockType
, <<"block_result_type">> => BlockResultType
- , <<"save_to">> => SaveTo
+ , <<"save_to">> => ?FORMATTING:serialize_maybe_undefined(SaveTo)
+ , <<"show_in_toolbox">> => ShowInToolbox
};
encode_block(#service_port_trigger_block{ block_id=BlockId
@@ -106,52 +106,54 @@ encode_block(#service_port_trigger_block{ block_id=BlockId
, expected_value=ExpectedValue
, key=Key
, subkey=SubKey
+ , show_in_toolbox=ShowInToolbox
}) ->
#{ <<"block_id">> => BlockId
, <<"function_name">> => FunctionName
, <<"message">> => Message
, <<"arguments">> => lists:map(fun encode_argument/1, Arguments)
, <<"block_type">> => BlockType
- , <<"save_to">> => SaveTo
- , <<"expected_value">> => ExpectedValue
- , <<"key">> => Key
- , <<"subkey">> => SubKey
- };
-
-%% TODO: Add DB migration to avoid the need of this compatibility
-encode_block({service_port_trigger_block
- , BlockId
- , FunctionName
- , Message
- , Arguments
- , BlockType
- , SaveTo
- , ExpectedValue
- , Key
- }) ->
- #{ <<"block_id">> => BlockId
- , <<"function_name">> => FunctionName
- , <<"message">> => Message
- , <<"arguments">> => lists:map(fun encode_argument/1, Arguments)
- , <<"block_type">> => BlockType
- , <<"save_to">> => SaveTo
+ , <<"save_to">> => ?FORMATTING:serialize_maybe_undefined(SaveTo)
, <<"expected_value">> => ExpectedValue
- , <<"key">> => Key
- , <<"subkey">> => undefined
+ , <<"key">> => ?FORMATTING:serialize_maybe_undefined(Key)
+ , <<"subkey">> => ?FORMATTING:serialize_maybe_undefined(SubKey)
+ , <<"show_in_toolbox">> => ShowInToolbox
}.
encode_argument(#service_port_block_static_argument{ type=Type
, default=Default
, class=Class
}) ->
- #{ <<"type">> => Type
- , <<"default_value">> => Default
- , <<"class">> => Class
- };
-
+ case Type of
+ {<<"variable">>, VarType} ->
+ #{ <<"type">> => <<"variable">>
+ , <<"default_value">> => Default
+ , <<"class">> => Class
+ , <<"var_type">> => VarType
+ };
+ _ ->
+ #{ <<"type">> => Type
+ , <<"default_value">> => Default
+ , <<"class">> => Class
+ }
+ end;
encode_argument(#service_port_block_dynamic_argument{ type=Type
, callback=Callback
}) ->
#{ <<"type">> => Type
, <<"callback">> => Callback
+ };
+
+encode_argument(#service_port_block_dynamic_sequence_argument{ type=Type
+ , callback_sequence=CallbackSequence
+ }) ->
+ #{ <<"type">> => Type
+ , <<"callback_sequence">> => CallbackSequence
+ };
+
+encode_argument(#service_port_block_collection_argument{ name=Collection
+ }) ->
+ #{ type => string
+ , callback => Collection
+ , collection => Collection
}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_custom_signals_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_custom_signals_root.erl
index d75ac886..edca8358 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_custom_signals_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_custom_signals_root.erl
@@ -16,10 +16,11 @@
, to_json/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
-include("../../automate_storage/src/records.hrl").
--record(state, { user_id }).
+-record(state, { user_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -43,7 +44,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[Custom Signals] Asking for methods~n", []),
{[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -52,13 +52,17 @@ is_authorized(Req, State) ->
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+ Method ->
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
+ Scope = case Method of
+ <<"GET">> -> list_custom_signals;
+ <<"POST">> -> create_custom_signals
+ end,
#state{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
{true, UserId} ->
{ true, Req1, State };
{true, _} -> %% Non matching user id
@@ -79,11 +83,11 @@ content_types_accepted(Req, State) ->
accept_json_create_signal(Req, State) ->
#state{user_id=UserId} = State,
- {ok, Body, Req1} = read_body(Req),
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
Signal = jiffy:decode(Body, [return_maps]),
#{ <<"name">> := SignalName } = Signal,
- case automate_rest_api_backend:create_custom_signal(UserId, SignalName) of
+ case automate_rest_api_backend:create_custom_signal({user, UserId}, SignalName) of
{ ok, SignalId } ->
Output = jiffy:encode(#{ <<"id">> => SignalId
@@ -103,11 +107,9 @@ content_types_provided(Req, State) ->
-spec to_json(cowboy_req:req(), #state{})
-> {binary(),cowboy_req:req(), #state{}}.
-to_json(Req, State) ->
- #state{user_id=UserId} = State,
- case automate_rest_api_backend:list_custom_signals_from_user_id(UserId) of
+to_json(Req, State=#state{user_id=UserId}) ->
+ case automate_storage:list_custom_signals({user, UserId}) of
{ ok, Signals } ->
-
Output = jiffy:encode(lists:map(fun signal_to_map/1, Signals)),
Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
@@ -115,23 +117,12 @@ to_json(Req, State) ->
{ Output, Res2, State }
end.
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
-
-
signal_to_map(#custom_signal_entry{ id=Id
, name=Name
- , owner=Owner
+ , owner={OwnerType, OwnerId}
}) ->
#{ id => Id
, name => Name
- , owner => Owner
+ , owner => OwnerId
+ , owner_full => #{ type => OwnerType, id => OwnerId}
}.
-
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_bridge_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_bridge_root.erl
new file mode 100644
index 00000000..b5185010
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_bridge_root.erl
@@ -0,0 +1,128 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_group_bridge_root).
+
+-export([init/2]).
+
+-export([ allowed_methods/2
+ , content_types_accepted/2
+ , is_authorized/2
+ , options/2
+ , resource_exists/2
+ , content_types_provided/2
+ ]).
+
+-export([ accept_json_create_service_port/2
+ , to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(URLS, automate_rest_api_utils_urls).
+
+-record(state, { group_id :: binary()
+ , user_id :: binary() | undefined
+ }).
+
+-spec init(_, _) -> {cowboy_rest, _, _}.
+
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ {cowboy_rest, Req,
+ #state{ group_id=GroupId
+ , user_id=undefined}}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> -> {false, Req, State};
+ _ -> {true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(), _) -> {[binary()], cowboy_req:req(), _}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> -> {true, Req1, State};
+ Method ->
+ {Check, Scope} = case Method of
+ <<"GET">> -> {fun automate_storage:is_allowed_to_read_in_group/2, { list_group_bridges, GroupId }};
+ _ -> {fun automate_storage:is_allowed_to_write_in_group/2, { create_group_bridges, GroupId }}
+ end,
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UserId} ->
+ case Check({user, UserId}, GroupId) of
+ true -> { true, Req1, State#state{ user_id=UserId } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{group_id=GroupId}) ->
+ case automate_service_port_engine:get_user_service_ports({group, GroupId}) of
+ { ok, Bridges } ->
+ Output = jiffy:encode(lists:map(fun ?FORMATTING:bridge_to_json/1, Bridges)),
+
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []},
+ accept_json_create_service_port}],
+ Req, State}.
+
+-spec accept_json_create_service_port(cowboy_req:req(),
+ #state{}) -> {{true,
+ binary()},
+ cowboy_req:req(),
+ #state{}}.
+accept_json_create_service_port(Req, State=#state{group_id=GroupId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ #{ <<"name">> := ServicePortName } = jiffy:decode(Body, [return_maps]),
+
+ case {ok, ServicePortId } = automate_service_port_engine:create_service_port({group, GroupId}, ServicePortName) of
+ {ok, ServicePortId} ->
+ Url = ?URLS:bridge_control_url(ServicePortId),
+
+ Output = jiffy:encode(#{ control_url => Url
+ , id => ServicePortId
+ }),
+ Res2 = cowboy_req:set_resp_body(Output, Req1),
+ Res3 = cowboy_req:delete_resp_header(<<"content-type">>,
+ Res2),
+ Res4 = cowboy_req:set_resp_header(<<"content-type">>,
+ <<"application/json">>, Res3),
+ {{true, Url}, Res4, State}
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_by_name.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_by_name.erl
new file mode 100644
index 00000000..1c6a95a8
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_by_name.erl
@@ -0,0 +1,79 @@
+%%% @doc
+%%% REST endpoint to manage groups.
+%%% @end
+
+-module(automate_rest_api_group_by_name).
+-export([init/2]).
+-export([ allowed_methods/2
+ , is_authorized/2
+ , content_types_provided/2
+ , options/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() | undefined
+ , group_name :: binary()
+ , group_info :: #user_group_entry{}
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupName = cowboy_req:binding(group_name, Req),
+ {ok, GroupInfo} = automate_storage:get_group_by_name(GroupName),
+ {cowboy_rest, Req, #state{ user_id=undefined, group_name=GroupName, group_info=GroupInfo }}.
+
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State=#state{ group_info=#user_group_entry{ id=GroupId } }) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { read_group_info, GroupId }) of
+ {true, UserId} ->
+ case automate_storage:can_user_view_as({user, UserId}, { group, GroupId }) of
+ true ->
+ { true, Req1, State#state{ user_id=UserId } };
+ false ->
+ { { false, <<"User cannot view this group">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{ group_info=GroupInfo }) ->
+ Output = jiffy:encode(#{ success => true, group => ?FORMATTING:group_to_json(GroupInfo)}),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_collaborators.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_collaborators.erl
new file mode 100644
index 00000000..d02af5ca
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_collaborators.erl
@@ -0,0 +1,126 @@
+%%% @doc
+%%% REST endpoint to manage group collaborators.
+%%% @end
+
+-module(automate_rest_api_group_collaborators).
+-export([init/2]).
+-export([ allowed_methods/2
+ , is_authorized/2
+ , content_types_provided/2
+ , content_types_accepted/2
+ , options/2
+ ]).
+
+-export([ to_json/2
+ , accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(PROGRAMS, automate_rest_api_utils_programs).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() | undefined, group_id :: binary()}).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ {cowboy_rest, Req, #state{ user_id=undefined, group_id=GroupId }}.
+
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ Method ->
+ {Check, Scope} = case Method of
+ <<"GET">> -> {fun automate_storage:is_allowed_to_read_in_group/2, {read_group_info, GroupId}};
+ _ -> {fun automate_storage:is_allowed_to_admin_in_group/2, {admin_group_info, GroupId}}
+ end,
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UserId} ->
+ case Check({user, UserId}, GroupId) of
+ true -> { true, Req1, State#state{ user_id=UserId } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{user_id=_UserId, group_id=GroupId}) ->
+ case automate_storage:list_collaborators({group, GroupId}) of
+ { ok, Collaborators } ->
+ Output = jiffy:encode(#{ success => true, collaborators => lists:map(fun ?FORMATTING:collaborator_to_json/1 , Collaborators)}),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }
+ end.
+
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+-spec accept_json(cowboy_req:req(), #state{})
+ -> {boolean(),cowboy_req:req(), #state{}}.
+accept_json(Req, State=#state{user_id=_UserId, group_id=GroupId}) ->
+ {ok, Body, _} = ?UTILS:read_body(Req),
+ #{ <<"action">> := Action, <<"collaborators">> := SerializedCollaborators } = jiffy:decode(Body, [return_maps]),
+
+ Collaborators = lists:map(fun read_collaborator/1, SerializedCollaborators),
+
+ Result = case Action of
+ <<"invite">> ->
+ automate_storage:add_collaborators({group, GroupId}, Collaborators);
+ <<"update">> ->
+ automate_storage:update_collaborators({group, GroupId}, Collaborators)
+ end,
+
+ case Result of
+ ok ->
+ Output = jiffy:encode(#{ success => true }),
+
+ Res1 = cowboy_req:set_resp_body(Output, Req),
+ Res2 = ?UTILS:send_json_format(Res1),
+
+ { true, Res2, State }
+ end.
+
+
+read_collaborator(#{ <<"id">> := Id
+ , <<"role">> := Role
+ }) ->
+ { Id, unwrap_role(Role) }.
+
+-spec unwrap_role(binary()) -> user_in_group_role().
+unwrap_role(<<"admin">>) -> admin;
+unwrap_role(<<"editor">>) -> editor;
+unwrap_role(<<"viewer">>) -> viewer.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_connections_available_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_connections_available_root.erl
new file mode 100644
index 00000000..fdc5cde6
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_connections_available_root.erl
@@ -0,0 +1,102 @@
+%%% @doc
+%%% REST endpoint to get available connection points
+%%% @end
+
+-module(automate_rest_api_group_connections_available_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+
+-record(state, { group_id :: binary(), user_id :: binary() | undefined }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ {cowboy_rest, Req
+ , #state{ group_id=GroupId
+ , user_id=undefined
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {list_group_connections_available, GroupId}) of
+ {true, UserId} ->
+ case automate_storage:is_allowed_to_write_in_group({user, UserId}, GroupId) of
+ true -> { true, Req1, State#state{ user_id=UserId } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{group_id=GroupId}) ->
+ case automate_rest_api_backend:list_available_connections({group, GroupId}) of
+ { ok, Connections } ->
+
+ Output = jiffy:encode(lists:map(fun to_map/1, Connections)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+to_map({#service_port_entry{ id=Id
+ , name=Name
+ , owner={OwnerType, OwnerId}
+ }
+ , #service_port_configuration{ service_id=ServiceId }
+ }) ->
+ #{ <<"id">> => Id
+ , <<"name">> => Name
+ , <<"owner">> => OwnerId
+ , <<"owner_full">> => #{ type => OwnerType, id => OwnerId }
+ , <<"service_id">> => ServiceId
+ }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_connections_established_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_connections_established_root.erl
new file mode 100644
index 00000000..7cf11b67
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_connections_established_root.erl
@@ -0,0 +1,92 @@
+%%% @doc
+%%% REST endpoint to get available connection points
+%%% @end
+
+-module(automate_rest_api_group_connections_established_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { group_id :: binary()
+ , user_id :: binary() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ {cowboy_rest, Req
+ , #state{ group_id=GroupId
+ , user_id=undefined
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {list_group_connections_established, GroupId}) of
+ {true, UserId} ->
+ case automate_storage:is_allowed_to_read_in_group({user, UserId}, GroupId) of
+ true -> { true, Req1, State#state{ user_id=UserId } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{group_id=GroupId}) ->
+ case automate_service_port_engine:list_established_connections({group, GroupId}) of
+ { ok, Connections } ->
+
+ Output = jiffy:encode(lists:filtermap(fun ?FORMATTING:connection_to_json/1, Connections)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_picture.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_picture.erl
new file mode 100644
index 00000000..514f157c
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_picture.erl
@@ -0,0 +1,107 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_group_picture).
+-export([ init/2
+ , allowed_methods/2
+ , content_types_provided/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ , resource_exists/2
+ , last_modified/2
+ ]).
+-export([ accept_file/2
+ , retrieve_file/2
+ ]).
+
+-include("./records.hrl").
+-define(UTILS, automate_rest_api_utils).
+
+-record(state, { group_id :: binary()
+ , last_modification_time :: undefined | calendar:datetime()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ {cowboy_rest, Req
+ , #state{ group_id=GroupId
+ , last_modification_time=undefined
+ }}.
+
+
+resource_exists(Req, State=#state{group_id=GroupId}) ->
+ case ?UTILS:group_picture_modification_time(GroupId) of
+ {error, not_found} ->
+ {false, Req, State};
+ { ok, ModTime }->
+ {true, Req, State#state{ last_modification_time=ModTime }}
+ end.
+
+last_modified(Req, State=#state{last_modification_time=ModTime}) ->
+ {ModTime, Req, State}.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
+
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ <<"GET">> ->
+ { true, Req1, State};
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {edit_group_picture, GroupId}) of
+ {true, UserId} ->
+ case automate_storage:is_allowed_to_admin_in_group({user, UserId}, GroupId) of
+ true -> { true, Req1, State };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"multipart">>, <<"form-data">>, []}, accept_file}],
+ Req, State}.
+
+-spec accept_file(cowboy_req:req(), #state{}) -> {boolean(),cowboy_req:req(), #state{}}.
+accept_file(Req, State=#state{group_id=GroupId}) ->
+ Path = ?UTILS:group_picture_path(GroupId),
+ {ok, _Data, Req1} = ?UTILS:stream_body_to_file(Req, Path, <<"file">>),
+ {true, Req1, State}.
+
+
+%% Image handler
+content_types_provided(Req, State) ->
+ {[{{<<"octet">>, <<"stream">>, []}, retrieve_file}],
+ Req, State}.
+
+-spec retrieve_file(cowboy_req:req(), #state{}) -> {stop | boolean(),cowboy_req:req(), #state{}}.
+retrieve_file(Req, State=#state{group_id=GroupId}) ->
+ Path = ?UTILS:group_picture_path(GroupId),
+ FileSize = filelib:file_size(Path),
+
+ Res = cowboy_req:reply(200, #{ %% <<"content-type">> => "image/png"
+ }, {sendfile, 0, FileSize, Path}, Req),
+ {stop, Res, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_profile_by_name.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_profile_by_name.erl
new file mode 100644
index 00000000..203f09f5
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_profile_by_name.erl
@@ -0,0 +1,81 @@
+%%% @doc
+%%% REST endpoint to retrieve a group's profile.
+%%% @end
+
+-module(automate_rest_api_group_profile_by_name).
+-export([init/2]).
+-export([ allowed_methods/2
+ , content_types_provided/2
+ , options/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+
+-record(state, { group_name :: binary()
+ , group_info :: #user_group_entry{}
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupName = cowboy_req:binding(group_name, Req),
+ {ok, GroupInfo} = automate_storage:get_group_by_name(GroupName),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1, #state{ group_name=GroupName
+ , group_info=GroupInfo
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{group_info=GroupInfo}) ->
+ #user_group_entry{ id=GroupId, name=GroupName } = GroupInfo,
+
+ {ok, Programs } = automate_storage:list_programs({group, GroupId}),
+ {ok, Bridges } = automate_service_port_engine:get_user_service_ports({group, GroupId}),
+ {ok, Collaborators } = automate_storage:list_public_collaborators(GroupId),
+
+ ProgramList = lists:filtermap(fun(Program) ->
+ case Program of
+ #user_program_entry{ visibility=public } ->
+ ProgramBridges = try automate_bot_engine:get_bridges_on_program(Program) of
+ {ok, Result} ->
+ Result
+ catch ErrNS:Error:StackTrace ->
+ automate_logging:log_platform(error, ErrNS, Error, StackTrace),
+ []
+ end,
+ {true, ?FORMATTING:program_listing_to_json(Program, ProgramBridges)};
+ _ ->
+ false
+ end
+ end, Programs),
+
+ Output = jiffy:encode(#{ name => GroupName
+ , id => GroupId
+ , programs => ProgramList
+ , collaborators => lists:map(fun ?FORMATTING:user_to_json/1, Collaborators)
+ , bridges => lists:map(fun ?FORMATTING:bridge_to_json/1, Bridges)
+ }),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_programs.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_programs.erl
new file mode 100644
index 00000000..f9d829ee
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_programs.erl
@@ -0,0 +1,116 @@
+%%% @doc
+%%% REST endpoint to manage group programs.
+%%% @end
+
+-module(automate_rest_api_group_programs).
+-export([init/2]).
+-export([ allowed_methods/2
+ , is_authorized/2
+ , content_types_accepted/2
+ , content_types_provided/2
+ , options/2
+ ]).
+
+-export([ to_json/2
+ , accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(PROGRAMS, automate_rest_api_utils_programs).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() | undefined, group_id :: binary()}).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ {cowboy_rest, Req, #state{ user_id=undefined, group_id=GroupId }}.
+
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ Method ->
+ {Check, Scope} = case Method of
+ <<"GET">> -> {fun automate_storage:is_allowed_to_read_in_group/2, { list_group_programs, GroupId }};
+ _ -> {fun automate_storage:is_allowed_to_write_in_group/2, { create_group_programs, GroupId }}
+ end,
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UserId} ->
+ case Check({user, UserId}, GroupId) of
+ true -> { true, Req1, State#state{ user_id=UserId } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{user_id=_UserId, group_id=GroupId}) ->
+ case automate_storage:list_programs({group, GroupId}) of
+ { ok, Programs } ->
+ Output = jiffy:encode(
+ #{ success => true
+ , programs => lists:map(fun (Program) ->
+ Bridges = try automate_bot_engine:get_bridges_on_program(Program) of
+ {ok, Result} ->
+ Result
+ catch ErrNS:Error:StackTrace ->
+ automate_logging:log_platform(error, ErrNS, Error, StackTrace),
+ []
+ end,
+ ?FORMATTING:program_listing_to_json(Program, Bridges)
+ end, Programs)}),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+-spec accept_json(cowboy_req:req(), #state{})
+ -> {boolean(),cowboy_req:req(), #state{}}.
+accept_json(Req, State=#state{user_id=_UserId, group_id=GroupId}) ->
+ {ok, Body, _} = ?UTILS:read_body(Req),
+ {Type, Name} = ?PROGRAMS:get_metadata_from_body(Body),
+ case automate_storage:create_program({group, GroupId}, Name, Type) of
+ { ok, ProgramId } ->
+ {ok, Program} = automate_storage:get_program_from_id(ProgramId),
+ Output = jiffy:encode(?FORMATTING:program_listing_to_json(Program)),
+
+ Res1 = cowboy_req:set_resp_body(Output, Req),
+ Res2 = ?UTILS:send_json_format(Res1),
+
+ { true, Res2, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_shared_resources.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_shared_resources.erl
new file mode 100644
index 00000000..832143b2
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_shared_resources.erl
@@ -0,0 +1,96 @@
+%%% @doc
+%%% REST endpoint to manage group programs.
+%%% @end
+
+-module(automate_rest_api_group_shared_resources).
+-export([init/2]).
+-export([ allowed_methods/2
+ , is_authorized/2
+ , content_types_provided/2
+ , options/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+
+-record(state, { group_id :: binary()}).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ {cowboy_rest, Req, #state{ group_id=GroupId }}.
+
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ <<"GET">> ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { list_group_shares, GroupId }) of
+ {true, UserId} ->
+ case automate_storage:is_allowed_to_read_in_group({user, UserId}, GroupId) of
+ true -> { true, Req1, State };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{group_id=GroupId}) ->
+ case automate_service_port_engine:get_resources_shared_with({group, GroupId}) of
+ { ok, Shares } ->
+ Data = lists:map(fun(#bridge_resource_share_entry{ connection_id=ConnectionId
+ , resource=Resource
+ , value=Value
+ }) ->
+ {ok, {ConnOwnerType, ConnOwnerId}} = automate_service_port_engine:get_connection_owner(ConnectionId),
+ {ok, BridgeId} = automate_service_port_engine:get_connection_bridge(ConnectionId),
+ {ok, #service_port_metadata{icon=Icon, name=Name}} = automate_service_port_engine:get_bridge_info(BridgeId),
+ #{ bridge_id => BridgeId
+ , icon => ?FORMATTING:serialize_icon(Icon)
+ , name => Name
+ , resource => Resource
+ , value_id => Value
+ , shared_by => #{ type => ConnOwnerType, id => ConnOwnerId}
+ }
+ end, Shares),
+ Output = jiffy:encode(
+ #{ success => true
+ , resources => Data
+ }),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_group_specific.erl b/backend/apps/automate_rest_api/src/automate_rest_api_group_specific.erl
new file mode 100644
index 00000000..0d0f623b
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_group_specific.erl
@@ -0,0 +1,124 @@
+%%% @doc
+%%% REST endpoint to manage groups.
+%%% @end
+
+-module(automate_rest_api_group_specific).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ , delete_resource/2
+ ]).
+
+-export([ accept_changes/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { group_id :: binary() }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ GroupId = cowboy_req:binding(group_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ group_id=GroupId
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { admin_group_info, GroupId }) of
+ {true, UId} ->
+ case automate_storage:is_allowed_to_admin_in_group({user, UId}, GroupId) of
+ true ->
+ { true, Req1, State };
+ false ->
+ { { false, <<"Action not authorized">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% Modifiers
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_changes}],
+ Req, State}.
+
+accept_changes(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"PATCH">> ->
+ update_group_metadata(Req, State)
+ end.
+
+%% PATCH handler
+update_group_metadata(Req, State=#state{group_id=GroupId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
+ case automate_storage:update_group_metadata(GroupId, body_to_metadata_edition(Parsed)) of
+ ok ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true }), Req),
+ { true, Req2, State };
+ { error, Reason } ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req1),
+ { false, Req2, State }
+ end.
+
+%% DELETE handler
+delete_resource(Req, State=#state{group_id=GroupId}) ->
+ case ?UTILS:group_has_picture(GroupId) of
+ true ->
+ ok = file:delete(?UTILS:group_picture_path(GroupId));
+ _ -> ok
+ end,
+ case automate_storage:delete_group(GroupId) of
+ ok ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
+ { true, Req1, State };
+ { error, Reason } ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req),
+ { false, Req1, State }
+ end.
+
+
+body_to_metadata_edition(Parsed) ->
+ AsList = lists:filtermap(fun({K, V}) ->
+ case K of
+ <<"public">> -> {true, {public, V} };
+ <<"min_level_for_private_bridge_usage">> -> {true, {min_level_for_private_bridge_usage, parse_collab_level(V)} };
+ _ -> false
+ end
+ end, maps:to_list(Parsed)),
+ maps:from_list(AsList).
+
+
+parse_collab_level(<<"not_allowed">>) ->
+ not_allowed;
+parse_collab_level(<<"admin">>) ->
+ admin;
+parse_collab_level(<<"editor">>) ->
+ editor;
+parse_collab_level(<<"viewer">>) ->
+ viewer.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_groups_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_groups_root.erl
new file mode 100644
index 00000000..af93fcdf
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_groups_root.erl
@@ -0,0 +1,100 @@
+%%% @doc
+%%% REST endpoint to manage groups.
+%%% @end
+
+-module(automate_rest_api_groups_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , is_authorized/2
+ , content_types_accepted/2
+ , options/2
+ ]).
+
+-export([ accept_json/2
+ ]).
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() | undefined}).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ {cowboy_rest, Req, #state{ user_id=undefined }}.
+
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, create_groups) of
+ {true, UserId} ->
+ { true, Req1, State#state{ user_id=UserId } };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+%% POST
+accept_json(Req, State=#state{ user_id=UserId }) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
+ Collaborators = case Parsed of
+ #{ <<"collaborators">> := Collabs } -> Collabs;
+ _ -> []
+ end,
+ case automate_storage:create_group(maps:get(<<"name">>, Parsed), UserId, maps:get(<<"public">>, Parsed)) of
+ {ok, Group=#user_group_entry{ id=GroupId }} ->
+ ok = automate_storage:add_collaborators({ group, GroupId }, lists:map(fun(#{ <<"id">> := UId, <<"role">> := RoleStr }) ->
+ Role = case RoleStr of
+ <<"admin">> -> admin;
+ <<"editor">> -> editor;
+ <<"viewer">> -> viewer
+ end,
+ {UId, Role}
+ end,
+ Collaborators)),
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ success => true
+ , group => ?FORMATTING:group_to_json(Group)
+ }), Req),
+ { true, Req2, State };
+ {error, already_exists} ->
+
+ Res = cowboy_req:reply(409, %% Conflict
+ #{ <<"content-type">> => <<"application/json">> },
+ jiffy:encode(#{ <<"success">> => false
+ , <<"error">> => already_exists
+ }),
+ Req1),
+ { stop, Res, State };
+ {error, Reason} ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false
+ , <<"error">> => unknown
+ , <<"debug">> => list_to_binary(lists:flatten(io_lib:format("~p", [Reason])))
+ })
+ , Req1),
+ { false, Req2, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_metrics.erl b/backend/apps/automate_rest_api/src/automate_rest_api_metrics.erl
index 80ac0d16..7f346957 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_metrics.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_metrics.erl
@@ -11,7 +11,6 @@
-export([ to_text/2
]).
--include("./records.hrl").
-define(APPLICATION, automate_rest_api).
-define(METRICS_BEARER_TOKEN_SETTING, metrics_secret).
@@ -33,7 +32,6 @@ is_authorized(Req, State) ->
<<"Bearer ", Secret/binary>> ->
{ true, Req, State };
X ->
- io:format("Expected ~p found ~p~n", [Secret, X]),
{ { false, <<"Authorization not correct">>}, Req, State }
end
end.
@@ -48,7 +46,14 @@ content_types_provided(Req, State) ->
-> {binary(),cowboy_req:req(), {}}.
to_text(Req, State) ->
- Output = automate_stats:format(prometheus),
- Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
- Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Res1),
- { Output, Res2, State }.
+ try automate_stats:format(prometheus) of
+ Output ->
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Res1),
+ { Output, Res2, State }
+ catch ErrorNS:Error:StackTrace ->
+ Code = 500,
+ automate_logging:log_platform(error, ErrorNS, Error, StackTrace),
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, <<"Error getting stats, check logs for more info">>, Req),
+ {stop, Res, State}
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_monitors_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_monitors_root.erl
index 05244c3f..43b16fcb 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_monitors_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_monitors_root.erl
@@ -16,9 +16,10 @@
, to_json/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(state, { username }).
+-record(state, { username :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -42,7 +43,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -51,13 +51,17 @@ is_authorized(Req, State) ->
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+ Method ->
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
+ Scope = case Method of
+ <<"GET">> -> list_monitors;
+ <<"POST">> -> create_monitors
+ end,
#state{username=Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ case automate_rest_api_backend:is_valid_token(X, Scope) of
{true, Username} ->
{ true, Req1, State };
{true, _} -> %% Non matching username
@@ -78,7 +82,7 @@ content_types_accepted(Req, State) ->
accept_json_create_monitor(Req, State) ->
#state{username=Username} = State,
- {ok, Body, Req1} = read_body(Req),
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
Parsed = [jiffy:decode(Body, [return_maps])],
Monitor = decode_monitor(Parsed),
@@ -105,16 +109,6 @@ decode_monitor([#{ <<"type">> := Type
, name=Name
}.
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
-
-
%% GET handler
content_types_provided(Req, State) ->
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_assets_by_id.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_assets_by_id.erl
new file mode 100644
index 00000000..09b2c8e5
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_assets_by_id.erl
@@ -0,0 +1,89 @@
+-module(automate_rest_api_program_assets_by_id).
+-export([ init/2
+ , allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+-export([ retrieve_file/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-define(UTILS, automate_rest_api_utils).
+-define(MAX_AGE_IMMUTABLE_SECONDS, 31536000). %% Seconds in a year
+
+-record(state, { owner_id :: owner_id() | undefined
+ , program_id :: binary()
+ , asset_id :: binary()
+ , asset_info :: #user_asset_entry{} | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ ProgramId = cowboy_req:binding(program_id, Req1),
+ AssetId = cowboy_req:binding(asset_id, Req1),
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ , asset_id=AssetId
+ , owner_id=undefined
+ , asset_info=undefined
+ }}.
+
+resource_exists(Req, State=#state{program_id=ProgramId, asset_id=AssetId}) ->
+ {ok, Owner} = automate_storage:get_program_owner(ProgramId),
+ case automate_storage:get_user_asset_info(Owner, AssetId) of
+ {error, not_found} ->
+ {false, Req, State};
+ {ok, AssetInfo} ->
+ {true, Req, State#state{owner_id=Owner, asset_info=AssetInfo}}
+ end.
+
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{owner_id=_OwnerId}) ->
+ case cowboy_req:method(Req) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req, State };
+ <<"GET">> ->
+ { true, Req, State }
+ end.
+
+
+%% Image handler
+content_types_provided(Req, State) ->
+ {[{{<<"octet">>, <<"stream">>, []}, retrieve_file}],
+ Req, State}.
+
+-spec retrieve_file(cowboy_req:req(), #state{}) -> {stop | boolean(),cowboy_req:req(), #state{}}.
+retrieve_file(Req, State=#state{ asset_id=AssetId
+ , owner_id=Owner
+ , asset_info=#user_asset_entry{mime_type=MimeType}
+ }) ->
+ Dir = ?UTILS:get_owner_asset_directory(Owner),
+ Path = list_to_binary([Dir, "/", AssetId]),
+ FileSize = filelib:file_size(Path),
+
+ ContentType = case MimeType of
+ { Type, undefined } ->
+ Type;
+ { Type, SubType } ->
+ list_to_binary([Type, "/", SubType])
+ end,
+
+ Res = cowboy_req:reply(200, #{ <<"content-type">> => ContentType
+ , <<"cache-control">> => list_to_binary(io_lib:fwrite("public, max-age=~p, immutable", [?MAX_AGE_IMMUTABLE_SECONDS]))
+ }, {sendfile, 0, FileSize, Path}, Req),
+ {stop, Res, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_assets_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_assets_root.erl
new file mode 100644
index 00000000..0624fce4
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_assets_root.erl
@@ -0,0 +1,131 @@
+-module(automate_rest_api_program_assets_root).
+-export([ init/2
+ , allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ ]).
+-export([ accept_file/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_common_types/src/types.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-define(UTILS, automate_rest_api_utils).
+
+-record(state, { owner_id :: owner_id() | undefined
+ , program_id :: binary()
+ , copy_from :: undefined | { binary(), binary() }
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ ProgramId = cowboy_req:binding(program_id, Req1),
+ Qs = cowboy_req:parse_qs(Req),
+ CopyFrom = case proplists:get_value(<<"copy_from">>, Qs, undefined) of
+ undefined -> undefined;
+ From when is_binary(From) ->
+ [FromProgramId, FromAssetId] = binary:split(From, <<"/">>),
+ {FromProgramId, FromAssetId}
+ end,
+
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ , owner_id=undefined
+ , copy_from=CopyFrom
+ }}.
+
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId, copy_from=CopyFrom}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, create_assets) of
+ {true, UId} ->
+ {ok, Owner} = automate_storage:get_program_owner(ProgramId),
+ case automate_storage:can_user_edit_as({user, UId}, Owner) of
+ true ->
+ case CopyFrom of
+ undefined ->
+ { true, Req1, State#state{owner_id=Owner} };
+ {FromProgram, _} ->
+ case automate_storage:is_user_allowed({user, UId}, FromProgram, read_program) of
+ {ok, true} ->
+ { true, Req1, State#state{owner_id=Owner} };
+ {ok, false} ->
+ { { false, <<"Cannot copy from source program">>}, Req1, State }
+ end
+ end;
+ false ->
+ { { false, <<"Action not authorized">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"multipart">>, <<"form-data">>, []}, accept_file}],
+ Req, State}.
+
+-spec accept_file(cowboy_req:req(), #state{}) -> {boolean(),cowboy_req:req(), #state{}}.
+accept_file(Req, State=#state{owner_id=OwnerId, copy_from={FromProgramId, FromAssetId}}) ->
+ {ok, FromProgramOwner} = automate_storage:get_program_owner(FromProgramId),
+
+ %% TODO: Implement REST check to return the appropriate HTTP code
+ case FromProgramOwner == OwnerId of
+ true ->
+ ok;
+ false ->
+ case automate_storage:get_user_asset_info(OwnerId, FromAssetId) of
+ {error, not_found} ->
+ {ok, #user_asset_entry{ mime_type=MimeType }} = automate_storage:get_user_asset_info(FromProgramOwner, FromAssetId),
+ ?UTILS:copy_asset(FromProgramOwner, OwnerId, FromAssetId),
+ ok = automate_storage:add_user_asset(OwnerId, FromAssetId, MimeType);
+ {ok, _AssetInfo} ->
+ %% No need to do anything, already exists
+ ok
+ end
+ end,
+ {true, Req, State};
+accept_file(Req, State=#state{owner_id=OwnerId}) ->
+ Path = ?UTILS:get_owner_asset_directory(OwnerId),
+ {ok, {AssetId, FileType}, Req1} = ?UTILS:stream_body_to_file_hashname(Req, Path, <<"file">>),
+
+ MimeType = case binary:split(FileType, <<"/">>) of
+ [Type, SubType] ->
+ {Type, SubType};
+ [Type] ->
+ {Type, undefined}
+ end,
+ ok = automate_storage:add_user_asset(OwnerId, AssetId, MimeType),
+
+ Output = jiffy:encode(#{ success => true
+ , value => AssetId
+ }),
+ Res2 = cowboy_req:set_resp_body(Output, Req1),
+ Res3 = cowboy_req:delete_resp_header(<<"content-type">>,
+ Res2),
+ Res4 = cowboy_req:set_resp_header(<<"content-type">>,
+ <<"application/json">>, Res3),
+ {true, Res4, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_bridge_callback.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_bridge_callback.erl
new file mode 100644
index 00000000..d2a50297
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_bridge_callback.erl
@@ -0,0 +1,102 @@
+%%% @doc
+%%% REST endpoint to manage bridge.
+%%% @end
+
+-module(automate_rest_api_program_bridge_callback).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { owner :: owner_id() | undefined
+ , program_id :: binary()
+ , bridge_id :: binary()
+ , callback :: binary()
+ , sequence_id :: binary() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ BridgeId = cowboy_req:binding(bridge_id, Req),
+ Callback = cowboy_req:binding(callback, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+
+ Qs = cowboy_req:parse_qs(Req1),
+ SequenceId = proplists:get_value(<<"sequence_id">>, Qs),
+
+ {cowboy_rest, Req1
+ , #state{ owner=undefined
+ , program_id=ProgramId
+ , bridge_id=BridgeId
+ , callback=Callback
+ , sequence_id=SequenceId
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId, bridge_id=BridgeId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { call_program_bridge_callback, BridgeId, ProgramId }) of
+ {true, UserId} ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:can_user_view_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+to_json(Req, State=#state{bridge_id=BridgeId, callback=Callback, owner=Owner, sequence_id=SequenceId}) ->
+ case automate_service_port_engine:callback_bridge(Owner, BridgeId, Callback, SequenceId) of
+ {ok, Result} ->
+ Output = jiffy:encode(#{ success => true, result => Result }),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State };
+ {error, Reason} ->
+ Code = case Reason of
+ not_found -> 404;
+ unauthorized -> 403;
+ no_connection -> 409; %% Conflict
+ _ -> 500
+ end,
+ Output = jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }),
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_available_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_available_root.erl
new file mode 100644
index 00000000..b79a65ab
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_available_root.erl
@@ -0,0 +1,107 @@
+%%% @doc
+%%% REST endpoint to get available connection points
+%%% @end
+
+-module(automate_rest_api_program_connections_available_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { owner :: owner_id() | undefined
+ , program_id :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ , owner=undefined
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {list_program_connections_available, ProgramId}) of
+ {true, UserId} ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:can_user_edit_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{owner=Owner}) ->
+ case automate_rest_api_backend:list_available_connections(Owner) of
+ { ok, Connections } ->
+
+ Output = jiffy:encode(lists:map(fun to_map/1, Connections)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+to_map({#service_port_entry{ id=Id
+ , name=Name
+ , owner={OwnerType, OwnerId}
+ }
+ , #service_port_configuration{ service_id=ServiceId }
+ }) ->
+ #{ <<"id">> => Id
+ , <<"name">> => Name
+ , <<"owner">> => OwnerId
+ , <<"owner_full">> => #{ type => OwnerType, id => OwnerId }
+ , <<"service_id">> => ServiceId
+ }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_established_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_established_root.erl
new file mode 100644
index 00000000..16f19b96
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_established_root.erl
@@ -0,0 +1,95 @@
+%%% @doc
+%%% REST endpoint to get available connection points
+%%% @end
+
+-module(automate_rest_api_program_connections_established_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { owner :: owner_id() | undefined
+ , program_id :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ , owner=undefined
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { list_program_connections_established, ProgramId }) of
+ {true, UserId} ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, read_program) of
+ {ok, true} -> { true, Req1, State#state{ owner=Owner } };
+ {ok, false} ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{owner=Owner}) ->
+ %% TODO: Filter connections not used on program
+ case automate_service_port_engine:list_established_connections(Owner) of
+ { ok, Connections } ->
+
+ Output = jiffy:encode(lists:filtermap(fun ?FORMATTING:connection_to_json /1, Connections)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_register_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_register_root.erl
new file mode 100644
index 00000000..9b38a51d
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_connections_register_root.erl
@@ -0,0 +1,96 @@
+-module(automate_rest_api_program_connections_register_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ ]).
+
+-export([ accept_json_register_service/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { program_id :: binary()
+ , service_id :: binary()
+ , owner :: owner_id() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ServiceId = cowboy_req:binding(service_id, Req),
+ ProgramId = cowboy_req:binding(program_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ owner=undefined
+ , program_id=ProgramId
+ , service_id=ServiceId
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {establish_program_connection, ProgramId}) of
+ {true, UserId} ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:can_user_edit_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []},
+ accept_json_register_service}],
+ Req, State}.
+
+-spec accept_json_register_service(cowboy_req:req(),
+ #state{}) -> {true, cowboy_req:req(), #state{}}.
+accept_json_register_service(Req, State=#state{owner=Owner, service_id=ServiceId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ FullRegistrationData = jiffy:decode(Body, [return_maps]),
+ { RegistrationData, ConnectionId } = case FullRegistrationData of
+ #{ <<"metadata">> := #{<<"connection_id">> := ConnId} } ->
+ {maps:remove(<<"metadata">>, FullRegistrationData), ConnId};
+ #{ <<"metadata">> := #{} } ->
+ {maps:remove(<<"metadata">>, FullRegistrationData), undefined};
+ _ ->
+ {FullRegistrationData, undefined}
+ end,
+ case send_registration_data(Owner, ServiceId, RegistrationData, ConnectionId) of
+ {ok, Data} ->
+ Output = jiffy:encode(Data),
+ Res2 = ?UTILS:send_json_output(Output, Req1),
+ {true, Res2, State}
+ end.
+
+send_registration_data(Owner, ServiceId, RegistrationData, ConnectionId) ->
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ {ok, _Result} = automate_service_registry_query:send_registration_data(Module, Owner, RegistrationData,
+ #{<<"connection_id">> => ConnectionId}).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_custom_blocks.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_custom_blocks.erl
new file mode 100644
index 00000000..cba03f82
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_custom_blocks.erl
@@ -0,0 +1,184 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_custom_blocks).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { program_id :: binary()
+ , owner :: owner_id() | undefined
+ , read_only :: boolean()
+ }).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ , read_only=true
+ }
+ }.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ Method ->
+ {ok, #user_program_entry{ visibility=Visibility }} = automate_storage:get_program_from_id(ProgramId),
+ IsPublic = ?UTILS:is_public(Visibility),
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ { true, Req1, State#state{ owner=Owner, read_only=true } };
+ _ ->
+ { {false, <<"Authorization header not found">>} , Req1, State }
+ end;
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { read_program, ProgramId }) of
+ {true, UserId} ->
+ case automate_storage:can_user_view_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner, read_only=false } };
+ false ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ { true, Req1, State#state{ owner=Owner, read_only=true } };
+ _ ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{owner=Owner, program_id=ProgramId, read_only=ReadOnly}) ->
+ %% TODO: When ReadOnly only show blocks used on the program
+ case automate_service_port_engine:list_custom_blocks(Owner) of
+ { ok, CustomBlocks } ->
+ Output = jiffy:encode(maps:map(fun encode_blocks/2, CustomBlocks)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+encode_blocks(_K, Blocks) ->
+ lists:map(fun encode_block/1, Blocks).
+
+encode_block(#service_port_block{ block_id=BlockId
+ , function_name=FunctionName
+ , message=Message
+ , arguments=Arguments
+ , block_type=BlockType
+ , block_result_type=BlockResultType
+ , save_to=SaveTo
+ , show_in_toolbox=ShowInToolbox
+ }) ->
+ #{ <<"block_id">> => BlockId
+ , <<"function_name">> => FunctionName
+ , <<"message">> => Message
+ , <<"arguments">> => lists:map(fun encode_argument/1, Arguments)
+ , <<"block_type">> => BlockType
+ , <<"block_result_type">> => BlockResultType
+ , <<"save_to">> => ?FORMATTING:serialize_maybe_undefined(SaveTo)
+ , <<"show_in_toolbox">> => ShowInToolbox
+ };
+
+encode_block(#service_port_trigger_block{ block_id=BlockId
+ , function_name=FunctionName
+ , message=Message
+ , arguments=Arguments
+ , block_type=BlockType
+ , save_to=SaveTo
+ , expected_value=ExpectedValue
+ , key=Key
+ , subkey=SubKey
+ , show_in_toolbox=ShowInToolbox
+ }) ->
+ #{ <<"block_id">> => BlockId
+ , <<"function_name">> => FunctionName
+ , <<"message">> => Message
+ , <<"arguments">> => lists:map(fun encode_argument/1, Arguments)
+ , <<"block_type">> => BlockType
+ , <<"save_to">> => ?FORMATTING:serialize_maybe_undefined(SaveTo)
+ , <<"expected_value">> => ExpectedValue
+ , <<"key">> => ?FORMATTING:serialize_maybe_undefined(Key)
+ , <<"subkey">> => ?FORMATTING:serialize_maybe_undefined(SubKey)
+ , <<"show_in_toolbox">> => ShowInToolbox
+ }.
+
+encode_argument(#service_port_block_static_argument{ type=Type
+ , default=Default
+ , class=Class
+ }) ->
+ case Type of
+ {<<"variable">>, VarType} ->
+ #{ <<"type">> => <<"variable">>
+ , <<"default_value">> => Default
+ , <<"class">> => Class
+ , <<"var_type">> => VarType
+ };
+ _ ->
+ #{ <<"type">> => Type
+ , <<"default_value">> => Default
+ , <<"class">> => Class
+ }
+ end;
+encode_argument(#service_port_block_dynamic_argument{ type=Type
+ , callback=Callback
+ }) ->
+ #{ <<"type">> => Type
+ , <<"callback">> => Callback
+ };
+
+encode_argument(#service_port_block_dynamic_sequence_argument{ type=Type
+ , callback_sequence=CallbackSequence
+ }) ->
+ #{ <<"type">> => Type
+ , <<"callback_sequence">> => CallbackSequence
+ };
+
+encode_argument(#service_port_block_collection_argument{ name=Collection
+ }) ->
+ #{ type => string
+ , callback => Collection
+ , collection => Collection
+ }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_logs.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_logs.erl
new file mode 100644
index 00000000..5fc0f507
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_logs.erl
@@ -0,0 +1,89 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_logs).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { program_id :: binary() }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {read_program_logs, ProgramId}) of
+ {true, UserId} ->
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program) of
+ {ok, true} ->
+ { true, Req1, State };
+ {ok, false} ->
+ { { false, <<"Not authorized">> }, Req1, State}
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State) ->
+ #state{ program_id=ProgramId} = State,
+ case automate_rest_api_backend:get_program_logs(ProgramId) of
+ { ok, ErrorLogs, UserLogs } ->
+ Output = jiffy:encode(?FORMATTING:serialize_logs(ErrorLogs, UserLogs)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_monitors_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_monitors_root.erl
new file mode 100644
index 00000000..cc6b02c6
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_monitors_root.erl
@@ -0,0 +1,107 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_monitors_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { program_id :: binary()
+ , owner :: owner_id() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ , owner=undefined
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { list_program_monitors, ProgramId }) of
+ {true, UserId} ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:can_user_view_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{owner=Owner}) ->
+ case automate_storage:list_monitors(Owner) of
+ { ok, Monitors } ->
+ Output = jiffy:encode(encode_monitors_list(Monitors)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+
+encode_monitors_list(Monitors) ->
+ encode_monitors_list(Monitors, []).
+
+encode_monitors_list([], Acc) ->
+ lists:reverse(Acc);
+
+encode_monitors_list([H | T], Acc) ->
+ #monitor_entry{ id=Id
+ , name=Name
+ } = H,
+ AsDictionary = #{ id => Id
+ , name => Name
+ },
+ encode_monitors_list(T, [AsDictionary | Acc]).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_render.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_render.erl
new file mode 100644
index 00000000..11eb8528
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_render.erl
@@ -0,0 +1,78 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_render).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { program_id :: binary()
+ , path :: binary()
+ , render_as :: page | element
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ Qs = cowboy_req:parse_qs(Req),
+ RenderAs = case proplists:get_value(<<"_render_as">>, Qs) of
+ <<"element">> ->
+ element;
+ <<"page">> ->
+ page;
+ _ ->
+ page
+ end,
+
+ Path = build_page_path(cowboy_req:path_info(Req)),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ , path=Path
+ , render_as=RenderAs
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=_ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ %% TODO: Require authentication?
+ {true, Req1, State}.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"html">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {iolist(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{program_id=ProgramId, path=Path, render_as=RenderAs}) ->
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"text/html">>, Res1),
+
+ {ok, #program_pages_entry{ contents=Contents }} = automate_storage:get_program_page(ProgramId, Path),
+
+ { automate_rest_api_renderer:render_page(ProgramId, Contents, Req, RenderAs), Res2, State }.
+
+
+build_page_path(Path) ->
+ list_to_binary(["/"] ++ lists:join("/", Path)).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_services_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_services_root.erl
new file mode 100644
index 00000000..6cadcbe9
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_services_root.erl
@@ -0,0 +1,154 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_services_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-include("../../automate_service_registry/src/records.hrl").
+
+-record(state, { owner :: owner_id() | undefined
+ , program_id :: binary()
+ , read_only :: boolean()
+ }).
+
+-define(UTILS, automate_rest_api_utils).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ , owner=undefined
+ , read_only=true
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ Method ->
+ {ok, #user_program_entry{ visibility=Visibility }} = automate_storage:get_program_from_id(ProgramId),
+ IsPublic = ?UTILS:is_public(Visibility),
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ { true, Req1, State#state{ owner=Owner, read_only=true } };
+ _ ->
+ { {false, <<"Authorization header not found">>} , Req1, State }
+ end;
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { list_program_services, ProgramId }) of
+ {true, UserId} ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:can_user_view_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner, read_only=false } };
+ false ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ { true, Req1, State#state{ owner=Owner, read_only=true } };
+ _ ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{owner=Owner, program_id=ProgramId, read_only=ReadOnly}) ->
+ %% TODO: When ReadOnly only show blocks used on the program
+ {ok, Services} = automate_service_registry:get_all_services_for_user(Owner),
+ {ok, SharedConnections} = automate_service_port_engine:get_resources_shared_with(Owner),
+
+ SharedBridges = lists:filtermap(fun(#bridge_resource_share_entry{ connection_id=ConnectionId }) ->
+ case automate_service_port_engine:get_connection_bridge(ConnectionId) of
+ {ok, BridgeId} -> {true, BridgeId};
+ {error, not_found} ->
+ automate_logging:log_api(error, ?MODULE, binary:list_to_bin(io_lib:format("Bridge not found for connection: ~p", [ConnectionId]))),
+ false
+ end
+ end, SharedConnections),
+ SharedServices = lists:map(fun(BridgeId) ->
+ {ok, #service_port_configuration{service_id=ServiceId}} = automate_service_port_engine:get_bridge_configuration(BridgeId),
+ {ok, Service} = automate_service_registry:get_service_by_id(ServiceId),
+ {ServiceId, Service}
+ end, sets:to_list(sets:from_list(SharedBridges))),
+
+ AllServices = merge_service_map(SharedServices,Services),
+
+ ServiceData = automate_rest_api_backend:get_services_metadata(AllServices, Owner),
+ Output = jiffy:encode(encode_service_list(ServiceData)),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }.
+
+merge_service_map([], Acc) ->
+ Acc;
+merge_service_map([{Id, Val} | T], Acc) ->
+ merge_service_map(T, Acc#{ Id => Val }).
+
+
+encode_service_list(Services) ->
+ encode_service_list(Services, []).
+
+
+encode_service_list([], Acc) ->
+ lists:reverse(Acc);
+
+encode_service_list([H | T], Acc) ->
+ #service_metadata{ id=Id
+ , name=Name
+ , link=Link
+ , enabled=Enabled
+ } = H,
+ AsDictionary = #{ <<"id">> => Id
+ , <<"name">> => Name
+ , <<"link">> => Link
+ , <<"enabled">> => Enabled
+ },
+ encode_service_list(T, [AsDictionary | Acc]).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_shared_resources.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_shared_resources.erl
new file mode 100644
index 00000000..04a24c58
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_shared_resources.erl
@@ -0,0 +1,108 @@
+%%% @doc
+%%% REST endpoint to pull shared resources from a program.
+%%% @end
+
+-module(automate_rest_api_program_shared_resources).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+
+-record(state, { program_id :: binary()
+ , owner_id :: owner_id()|undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ , owner_id=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { list_program_shares, ProgramId }) of
+ {true, UserId} ->
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, read_program) of
+ {ok, true} ->
+ {ok, Owner} = automate_storage:get_program_owner(ProgramId),
+ { true, Req1, State#state{owner_id=Owner} };
+ {ok, false} ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{owner_id=Owner}) ->
+ case automate_service_port_engine:get_resources_shared_with(Owner) of
+ { ok, Shares } ->
+ Data = lists:filtermap(fun(#bridge_resource_share_entry{ connection_id=ConnectionId
+ , resource=Resource
+ , value=Value
+ }) ->
+ case automate_service_port_engine:get_connection_owner(ConnectionId) of
+ {ok, {ConnOwnerType, ConnOwnerId}} ->
+ {ok, BridgeId} = automate_service_port_engine:get_connection_bridge(ConnectionId),
+ {ok, #service_port_metadata{icon=Icon, name=Name}} = automate_service_port_engine:get_bridge_info(BridgeId),
+ { true, #{ bridge_id => BridgeId
+ , icon => ?FORMATTING:serialize_icon(Icon)
+ , name => Name
+ , resource => Resource
+ , value_id => Value
+ , shared_by => #{ type => ConnOwnerType, id => ConnOwnerId}
+ }};
+ {error, not_found} ->
+ automate_logging:log_api(error, ?MODULE, binary:list_to_bin(io_lib:format("Bridge not found for connection: ~p", [ConnectionId]))),
+ false
+ end
+ end, Shares),
+ Output = jiffy:encode(
+ #{ success => true
+ , resources => Data
+ }),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_by_id.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_by_id.erl
new file mode 100644
index 00000000..d0d26574
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_by_id.erl
@@ -0,0 +1,207 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_specific_by_id).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , content_types_accepted/2
+ , delete_resource/2
+ ]).
+
+-export([ to_json/2
+ , accept_json_program/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { program_id :: binary(), user_id :: binary() | undefined }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ , user_id=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+
+ %% Reading a public program
+ Method ->
+ {ok, #user_program_entry{ visibility=Visibility }} = automate_storage:get_program_from_id(ProgramId),
+ IsPublic = ?UTILS:is_public(Visibility),
+ {Action, Scope} = case Method of
+ <<"GET">> -> {read_program, { read_program, ProgramId }};
+ <<"PUT">> -> {edit_program, { edit_program, ProgramId }};
+ <<"PATCH">> -> {edit_program, { edit_program_metadata, ProgramId }};
+ <<"DELETE">> -> {delete_program, { delete_program, ProgramId }}
+ end,
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ { true, Req1, State };
+ _ ->
+ { {false, <<"Authorization header not found">>} , Req1, State }
+ end;
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UId} ->
+ case automate_storage:is_user_allowed({user, UId}, ProgramId, Action) of
+ {ok, true} ->
+ { true, Req1, State#state{user_id=UId} };
+ {ok, false} ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ {true, Req1, State#state{user_id=UId}};
+ _ ->
+ { { false, <<"Action not authorized">>}, Req1, State }
+ end;
+ {error, Reason} ->
+ automate_logging:log_api(warning, ?MODULE, {authorization_error, Reason}),
+ { { false, <<"Error on authorization">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{program_id=ProgramId, user_id=UserId}) ->
+ Qs = cowboy_req:parse_qs(Req),
+ IncludePages = case proplists:get_value(<<"retrieve_pages">>, Qs) of
+ <<"yes">> ->
+ true;
+ _ ->
+ false
+ end,
+
+ case automate_rest_api_backend:get_program(ProgramId) of
+ { ok, Program=#user_program{last_upload_time=ProgramTime} } ->
+ Checkpoint = case automate_storage:get_last_checkpoint_content(ProgramId) of
+ {ok, #user_program_checkpoint{event_time=CheckpointTime, content=Content} } ->
+ case ProgramTime < (CheckpointTime / 1000) of
+ true ->
+ Content;
+ false ->
+ null
+ end;
+ {error, not_found} ->
+ null
+ end,
+
+ Json = ?FORMATTING:program_data_to_json(Program, Checkpoint),
+
+ {ok, CanEdit} = automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program),
+ {ok, CanAdmin } = automate_storage:is_user_allowed({user, UserId}, ProgramId, admin_program),
+
+ Json2 = Json#{ readonly => not CanEdit, can_admin => CanAdmin },
+ Json3 = case IncludePages of
+ false -> Json2;
+ true ->
+ {ok, Pages} = automate_storage:get_program_pages(ProgramId),
+ Json#{ pages => maps:from_list(lists:map(fun (#program_pages_entry{ page_id={_, Path}
+ , contents=Contents}) ->
+ {Path, Contents}
+ end, Pages))
+ }
+ end,
+
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { jiffy:encode(Json3), Res2, State }
+ end.
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json_program}],
+ Req, State}.
+
+accept_json_program(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"PUT">> ->
+ update_program(Req, State);
+ <<"PATCH">> ->
+ update_program_metadata(Req, State)
+ end.
+
+%% PUT handler
+update_program(Req, State=#state{program_id=ProgramId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
+ Program = decode_program(Parsed),
+ case automate_rest_api_backend:update_program_by_id(ProgramId, Program) of
+ ok ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true }), Req),
+ { true, Req2, State };
+ { error, Reason } ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req1),
+ { false, Req2, State }
+ end.
+
+%% PATCH handler
+update_program_metadata(Req, State=#state{program_id=ProgramId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
+ case automate_rest_api_backend:update_program_metadata(ProgramId, Parsed) of
+ ok ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true }), Req),
+ { true, Req2, State };
+ { error, Reason } ->
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req1),
+ { false, Req2, State }
+ end.
+
+%% DELETE handler
+delete_resource(Req, State=#state{program_id=ProgramId}) ->
+ case automate_rest_api_backend:delete_program(ProgramId) of
+ ok ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
+ { true, Req1, State };
+ { error, Reason } ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req),
+ { false, Req1, State }
+ end.
+
+
+%% Converters
+decode_program(P=#{ <<"type">> := ProgramType
+ , <<"orig">> := ProgramOrig
+ , <<"parsed">> := ProgramParsed
+ }) ->
+ #program_content { type=ProgramType
+ , orig=ProgramOrig
+ , parsed=ProgramParsed
+ , pages=case P of
+ #{ <<"pages">> := Pags } -> Pags;
+ _ -> #{}
+ end
+ }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_checkpoint.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_checkpoint.erl
new file mode 100644
index 00000000..dce1de71
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_checkpoint.erl
@@ -0,0 +1,94 @@
+%%% @doc
+%%% REST endpoint to manipulate program checkpoints.
+%%% @end
+
+-module(automate_rest_api_program_specific_checkpoint).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ ]).
+
+-export([ accept_json_program/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { program_id :: binary()
+ , user_id :: binary() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ , user_id=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { edit_program, ProgramId }) of
+ {true, UserId} ->
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program) of
+ {ok, true} ->
+ { true, Req1, State#state{user_id=UserId} };
+ {ok, false} ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json_program}],
+ Req, State}.
+
+accept_json_program(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ checkpoint_program(Req, State)
+ end.
+
+%% POST handler
+checkpoint_program(Req, State=#state{program_id=ProgramId, user_id=UserId}) ->
+
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
+ case automate_storage:checkpoint_program(UserId, ProgramId, Parsed) of
+ ok ->
+ Req2 = send_json_output(jiffy:encode(#{ <<"success">> => true }), Req),
+ { true, Req2, State };
+ { error, Reason } ->
+ Req2 = send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req1),
+ { false, Req2, State }
+ end.
+
+send_json_output(Output, Req) ->
+ Res1 = cowboy_req:set_resp_body(Output, Req),
+ Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
+ cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_editor_events.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_editor_events.erl
new file mode 100644
index 00000000..ef8ea5a1
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_editor_events.erl
@@ -0,0 +1,182 @@
+%%% @doc
+%%% WebSocket endpoint to listen to updates on a program.
+%%% @end
+
+-module(automate_rest_api_program_specific_editor_events).
+-export([init/2]).
+-export([websocket_init/1]).
+-export([websocket_handle/2]).
+-export([websocket_info/2]).
+
+
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(PING_INTERVAL_MILLISECONDS, 15000).
+
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() | none
+ , program_id :: binary()
+ , error :: none | binary()
+ , channel_id :: none | binary()
+ , can_edit :: boolean()
+ , skip_previous :: boolean()
+ }).
+
+
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+
+ Qs = cowboy_req:parse_qs(Req),
+ SkipPrevious = case proplists:get_value(<<"skip_previous">>, Qs, undefined) of
+ <<"t">> -> true;
+ <<"true">> -> true;
+ <<"1">> -> true;
+ _ -> false
+ end,
+
+ {Error, UserId, CanEdit} = case proplists:get_value(<<"token">>, Qs, undefined) of
+ undefined ->
+ {<<"Authorization header not found">>, none, false};
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {edit_program, ProgramId}) of
+ {true, TokenUserId} ->
+ {ok, UserCanView} = automate_storage:is_user_allowed({user, TokenUserId}, ProgramId, read_program),
+ {ok, UserCanEdit} = automate_storage:is_user_allowed({user, TokenUserId}, ProgramId, edit_program),
+ case UserCanView of
+ true -> {none, TokenUserId, UserCanEdit};
+ false ->
+ automate_logging:log_api(error, ?MODULE, {not_authorized, TokenUserId}),
+ {<<"Unauthorized to use this resource">>, TokenUserId, false}
+ end;
+ false ->
+ <<"Authorization not correct">>
+ end
+ end,
+ {cowboy_websocket, Req, #state{ program_id=ProgramId
+ , user_id=UserId
+ , error=Error
+ , channel_id=none
+ , can_edit=CanEdit
+ , skip_previous=SkipPrevious
+ }}.
+
+websocket_init(State=#state{ program_id=ProgramId
+ , error=none
+ , skip_previous=SkipPrevious
+ }) ->
+
+ {ok, #user_program_entry{ program_channel=ProgramChannelId }} = automate_storage:get_program_from_id(ProgramId),
+
+ automate_logging:log_api(debug, ?MODULE,
+ io_lib:format("Listening on program ~p; channel: ~p~n", [ProgramId, ProgramChannelId])),
+ {ok, ChannelId} = case automate_channel_engine:listen_channel(ProgramChannelId) of
+ ok -> {ok, ProgramChannelId};
+ {error, channel_not_found} ->
+ automate_logging:log_api(warning, ?MODULE, {fixing, program_channel, ProgramId}),
+ ok = automate_storage:fix_program_channel(ProgramId),
+ {ok, #user_program_entry{ program_channel=NewChannelId }} = automate_storage:get_program_from_id(ProgramId),
+ {automate_channel_engine:listen_channel(NewChannelId), NewChannelId}
+ end,
+ ok = automate_channel_engine:monitor_listeners(ChannelId, self(), node()),
+
+ Events = case SkipPrevious of
+ true -> [];
+ false -> case automate_storage:get_program_events(ProgramId) of
+ {ok, Evs} ->
+ lists:map(fun(#user_program_editor_event{ event=Ev }) ->
+ {text, jiffy:encode(Ev)}
+ end, Evs)
+ end
+ end,
+
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+
+ EndMarker = jiffy:encode(#{ <<"type">> => <<"editor_event">>
+ , <<"value">> => #{ <<"type">> => <<"ready">>
+ , <<"value">> => #{}
+ }
+ }),
+
+ {reply, Events ++ [{text, EndMarker}], State#state{ channel_id=ChannelId }};
+
+websocket_init(State=#state{error=Error}) ->
+ automate_logging:log_api(warning, ?MODULE,
+ io_lib:format("Closing with error: ~p~n", [Error])),
+ { reply
+ , { close, binary:list_to_bin(
+ lists:flatten(io_lib:format("Error: ~s", [Error]))) }
+ , State
+ }.
+
+
+websocket_handle({Type, _}, State=#state{can_edit=false}) when (Type == text) orelse (Type == binary) ->
+ {reply
+ , { close, <<"Not authorized to send events">> }
+ , State
+ };
+websocket_handle({Type, Message}, State=#state{program_id=ProgramId, channel_id=ChannelId}) when (Type == text) orelse (Type == binary) ->
+ Decoded = jiffy:decode(Message, [return_maps]),
+ case Decoded of
+ #{ <<"type">> := <<"editor_event">> } ->
+ ok = automate_channel_engine:send_to_channel(ChannelId, Decoded#{ from_id => self() }),
+ ok = case Decoded of
+ #{ <<"value">> := #{ <<"save">> := true } } ->
+ automate_storage:store_program_event(ProgramId, Decoded);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ {ok, State};
+websocket_handle(_, State) ->
+ {ok, State}.
+
+websocket_info({ automate_channel_engine, add_listener, {Pid, _Key, _SubKey}}, State) ->
+ Self = self(),
+ case Pid of
+ Self ->
+ {ok, State};
+ _ ->
+ {reply, {text, jiffy:encode(#{ <<"type">> => <<"editor_event">>
+ , <<"value">> => #{ <<"type">> => <<"add_editor">>
+ , <<"value">> => #{ <<"id">> => list_to_binary(pid_to_list(Pid)) }
+ }
+ })}, State}
+ end;
+
+websocket_info({automate_channel_engine,remove_listener, {Pid, _Channel}}, State) ->
+ Self = self(),
+ case Pid of
+ Self ->
+ {ok, State};
+ _ ->
+ {reply, {text, jiffy:encode(#{ <<"type">> => <<"editor_event">>
+ , <<"value">> => #{ <<"type">> => <<"remove_editor">>
+ , <<"value">> => #{ <<"id">> => list_to_binary(pid_to_list(Pid)) }
+ }
+ })}, State}
+ end;
+
+websocket_info(ping_interval, State) ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {reply, ping, State};
+
+websocket_info({channel_engine, _ChannelId, Message=#{ <<"type">> := <<"editor_event">> }}, State) ->
+ Pid = self(),
+ case Message of
+ #{ from_id := Pid } -> % Ignore if the message came from this websocket
+ {ok, State};
+ _ ->
+ NewMessage = case Message of
+ #{ from_id := NewPid, <<"value">> := Value=#{ <<"value">> := InnerValue }} ->
+ Clean = maps:remove(from_id, Message),
+ Clean#{ <<"value">> => Value#{ <<"value">> => InnerValue#{ <<"id">> => list_to_binary(pid_to_list(NewPid)) }}};
+ _ ->
+ Message
+ end,
+ {reply, {text, jiffy:encode(NewMessage)}, State}
+ end;
+
+websocket_info(_Message, State) ->
+ {ok, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_logs_stream.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_logs_stream.erl
new file mode 100644
index 00000000..94779bb8
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_logs_stream.erl
@@ -0,0 +1,97 @@
+%%% @doc
+%%% WebSocket endpoint to listen to updates on a program.
+%%% @end
+
+-module(automate_rest_api_program_specific_logs_stream).
+-export([init/2]).
+-export([websocket_init/1]).
+-export([websocket_handle/2]).
+-export([websocket_info/2]).
+
+
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(PING_INTERVAL_MILLISECONDS, 15000).
+
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary()
+ , program_id :: binary()
+ , error :: none | binary()
+ }).
+
+
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+
+ Qs = cowboy_req:parse_qs(Req),
+ {Error, UserId} = case proplists:get_value(<<"token">>, Qs, undefined) of
+ undefined ->
+ {<<"Authorization header not found">>, none};
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {read_program_logs, ProgramId}) of
+ {true, TokenUserId} ->
+ case automate_storage:is_user_allowed({user, TokenUserId}, ProgramId, edit_program) of
+ {ok, true} -> {none, TokenUserId};
+ {ok, false} -> automate_logging:log_api(warning, ?MODULE,
+ io_lib:format("[WS/Program] Token UID: ~p~n", [TokenUserId])),
+ {<<"Unauthorized to use this resource">>, none}
+ end;
+ false ->
+ {<<"Authorization not correct">>, none}
+ end
+ end,
+ {cowboy_websocket, Req, #state{ program_id=ProgramId
+ , user_id=UserId
+ , error=Error
+ }}.
+
+websocket_init(State=#state{ program_id=ProgramId
+ , error=none
+ }) ->
+
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+
+ automate_logging:log_api(debug, ?MODULE,
+ io_lib:format("[WS/Program] Listening on program ~p; channel: ~p~n", [ProgramId, ChannelId])),
+ ok = automate_channel_engine:listen_channel(ChannelId),
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+
+ {ok, State};
+
+websocket_init(State=#state{error=Error}) ->
+ automate_logging:log_api(warning, ?MODULE, {"[WS/Program] Closing with error", Error}),
+ { reply
+ , { close, binary:list_to_bin(
+ lists:flatten(io_lib:format("Error: ~s", [Error]))) }
+ , State
+ }.
+
+websocket_handle(ping, State) ->
+ {ok, State};
+websocket_handle({ping, _}, State) ->
+ {ok, State};
+websocket_handle(pong, State) ->
+ {ok, State};
+websocket_handle({pong, _}, State) ->
+ {ok, State};
+websocket_handle(Message, State) ->
+ automate_logging:log_api(warning, ?MODULE, {unexpected_message, Message}),
+ {ok, State}.
+
+
+websocket_info(ping_interval, State) ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {reply, ping, State};
+
+websocket_info({channel_engine, _ChannelId, Message}, State) when is_record(Message, user_program_log_entry) orelse is_record(Message, user_generated_log_entry) ->
+ case ?FORMATTING:format_message(Message) of
+ {ok, Structured} ->
+ {reply, {text, jiffy:encode(Structured)}, State};
+ {error, Reason} ->
+ automate_logging:log_api(error, ?MODULE, {"Error formatting", Reason, Message}),
+ {ok, State}
+ end;
+
+websocket_info(_Message, State) ->
+ %% io:fwrite("[D: ???]"),
+ {ok, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_ui_events.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_ui_events.erl
new file mode 100644
index 00000000..cb757d12
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_ui_events.erl
@@ -0,0 +1,126 @@
+%%% @doc
+%%% WebSocket endpoint to listen to updates on a program.
+%%% @end
+
+-module(automate_rest_api_program_specific_ui_events).
+-export([init/2]).
+-export([websocket_handle/2]).
+-export([websocket_info/2]).
+
+
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(PING_INTERVAL_MILLISECONDS, 15000).
+
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() | none
+ , program_id :: binary()
+ , authenticated :: boolean()
+ , can_edit :: boolean()
+ , channel_id :: binary() | none
+ }).
+
+
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_websocket, Req, #state{ program_id=ProgramId
+ , user_id=none
+ , authenticated=false
+ , can_edit=false
+ , channel_id=none
+ }}.
+
+
+websocket_handle({text, Msg}, State) ->
+ handle_message(Msg, State);
+
+websocket_handle({binary, Msg}, State) ->
+ handle_message(Msg, State);
+
+websocket_handle(_Message, State) ->
+ {ok, State}.
+
+websocket_info(ping_interval, State) ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {reply, ping, State};
+
+websocket_info({channel_engine, _ChannelId, Message=#{ <<"key">> := ui_events_show }}, State) ->
+ {reply, {text, jiffy:encode(Message)}, State};
+
+websocket_info(Message, State) ->
+ io:fwrite("Unexpected UI event: ~p~n", [Message]),
+ {ok, State}.
+
+%% Message handling
+handle_message(Msg, State=#state{ program_id=ProgramId
+ , authenticated=false
+ }) ->
+ Data = jiffy:decode(Msg, [return_maps]),
+ Passed = case Data of
+ #{ <<"type">> := <<"AUTHENTICATION">>
+ , <<"value">> := #{ <<"token">> := <<"ANONYMOUS">>
+ }
+ } -> %% TODO: Check that the program/page is publicly available
+ {true, false};
+ #{ <<"type">> := <<"AUTHENTICATION">>
+ , <<"value">> := #{ <<"token">> := Token
+ }
+ } ->
+ case automate_rest_api_backend:is_valid_token_uid(Token, { render_program, ProgramId }) of
+ {true, TokenUserId} ->
+ {ok, UserCanView} = automate_storage:is_user_allowed({user, TokenUserId}, ProgramId, read_program),
+ {ok, UserCanEdit} = automate_storage:is_user_allowed({user, TokenUserId}, ProgramId, edit_program),
+ case UserCanView of
+ true -> {true, UserCanEdit};
+ false ->
+ automate_logging:log_api(error, ?MODULE, {not_authorized, TokenUserId}),
+ {false, unauthorized}
+ end;
+ false ->
+ <<"Authorization not correct">>
+ end;
+ _ ->
+ {false, not_found}
+ end,
+ case Passed of
+ {true, CanEdit} ->
+ %% Initialize connection. Listen program and set ping
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+ ok = automate_channel_engine:listen_channel(ChannelId, {ui_events_show, undefined}),
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+
+ {ok, State#state{ authenticated=true, can_edit=CanEdit, channel_id=ChannelId }};
+ {false, Reason} ->
+ automate_logging:log_api(warning, ?MODULE,
+ list_to_binary(io_lib:format("UI Authentication error on program_id=~p (~p)",
+ [ ProgramId, Reason ]))),
+ { reply
+ , { close
+ , case Reason of
+ not_found -> <<"Token not found">>;
+ unauthorized -> <<"Not authorized to connect">>
+ end
+ }
+ , State
+ }
+ end;
+
+handle_message(Msg, State=#state{ channel_id=ChannelId }) ->
+ Data = jiffy:decode(Msg, [return_maps]),
+ case Data of
+ #{ <<"type">> := <<"ui-event">>
+ , <<"value">> := #{ <<"action">> := Action
+ , <<"block_type">> := BlockType
+ , <<"block_id">> := BlockId
+ , <<"data">> := UiData
+ }
+ } ->
+ ok = automate_channel_engine:send_to_channel(ChannelId, #{ <<"key">> => ui_events
+ , <<"subkey">> => <>
+ , <<"value">> => #{ <<"action">> => Action
+ , <<"connection">> => self()
+ , <<"ui_data">> => UiData
+ }
+ })
+ end,
+ {ok, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_ui_values.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_ui_values.erl
new file mode 100644
index 00000000..70897589
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_ui_values.erl
@@ -0,0 +1,80 @@
+-module(automate_rest_api_program_specific_ui_values).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { program_id :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ Action = read_program,
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {render_program, ProgramId}) of
+ {true, UId} ->
+ case automate_storage:is_user_allowed({user, UId}, ProgramId, Action) of
+ {ok, true} ->
+ { true, Req1, State };
+ {ok, false} ->
+ { { false, <<"Action not authorized">>}, Req1, State };
+ {error, Reason} ->
+ automate_logging:log_api(warning, ?MODULE, {authorization_error, Reason}),
+ { { false, <<"Error on authorization">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {iolist(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{program_id=ProgramId}) ->
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ {ok, Values} = automate_storage:get_widget_values_in_program(ProgramId),
+
+ { jiffy:encode(#{ success => true, widget_values => Values }), Res2, State }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_variables_stream.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_variables_stream.erl
new file mode 100644
index 00000000..dd36599d
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_specific_variables_stream.erl
@@ -0,0 +1,101 @@
+%%% @doc
+%%% WebSocket endpoint to listen to updates on a program.
+%%% @end
+
+-module(automate_rest_api_program_specific_variables_stream).
+-export([init/2]).
+-export([websocket_init/1]).
+-export([websocket_handle/2]).
+-export([websocket_info/2]).
+
+
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(PING_INTERVAL_MILLISECONDS, 15000).
+
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary()
+ , program_id :: binary()
+ , error :: none | binary()
+ }).
+
+
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+
+ Qs = cowboy_req:parse_qs(Req),
+ {Error, UserId} = case proplists:get_value(<<"token">>, Qs, undefined) of
+ undefined ->
+ {<<"Authorization header not found">>, none};
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {read_program_variables, ProgramId}) of
+ {true, TokenUserId} ->
+ case automate_storage:is_user_allowed({user, TokenUserId}, ProgramId, edit_program) of
+ {ok, true} -> {none, TokenUserId};
+ {ok, false} -> automate_logging:log_api(warning, ?MODULE,
+ io_lib:format("[WS/Program] Token UID: ~p~n", [TokenUserId])),
+ {<<"Unauthorized to use this resource">>, none}
+ end;
+ false ->
+ {<<"Authorization not correct">>, none}
+ end
+ end,
+ {cowboy_websocket, Req, #state{ program_id=ProgramId
+ , user_id=UserId
+ , error=Error
+ }}.
+
+websocket_init(State=#state{ program_id=ProgramId
+ , error=none
+ }) ->
+ {ok, #user_program_entry{ program_channel=ChannelId }} = automate_storage:get_program_from_id(ProgramId),
+
+ ok = automate_channel_engine:listen_channel(ChannelId, { variable_events, undefined }),
+
+ automate_logging:log_api(info, ?MODULE,
+ binary:list_to_bin(io_lib:format("[WS/Program/Variables] Listening on program ~p; channel: ~p~n", [ProgramId, ChannelId]))),
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+
+ {ok, State};
+
+websocket_init(State=#state{error=Error}) ->
+ automate_logging:log_api(warning, ?MODULE, {"[WS/Program/Variables] Closing with error", Error}),
+ { reply
+ , { close, binary:list_to_bin(
+ lists:flatten(io_lib:format("Error: ~s", [Error]))) }
+ , State
+ }.
+
+websocket_handle(ping, State) ->
+ {ok, State};
+websocket_handle({ping, _}, State) ->
+ {ok, State};
+websocket_handle(pong, State) ->
+ {ok, State};
+websocket_handle({pong, _}, State) ->
+ {ok, State};
+websocket_handle(Message, State) ->
+ automate_logging:log_api(warning, ?MODULE, {unexpected_message, Message}),
+ {ok, State}.
+
+
+websocket_info(ping_interval, State) ->
+ erlang:send_after(?PING_INTERVAL_MILLISECONDS, self(), ping_interval),
+ {reply, ping, State};
+
+websocket_info({channel_engine, _ChannelId, #{ <<"key">> := variable_events
+ , <<"subkey">> := ReceivedVariable
+ , <<"value">> := Value
+ }}, State) when is_binary(ReceivedVariable) or is_list(ReceivedVariable) ->
+ Update = #{ name => ReceivedVariable, value => Value },
+ try jiffy:encode(Update) of
+ Encoded when is_binary(Encoded) ->
+ {reply, {text, Encoded}, State}
+ catch ErrorNS:Error:ST ->
+ automate_logging:log_platform(error, ErrorNS, Error, ST),
+ {reply, {text, jiffy:encode(#{ name => ReceivedVariable, value => unknown })}, State}
+ end;
+
+websocket_info(_Message, State) ->
+ %% io:fwrite("[D: ???]"),
+ {ok, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_status.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_status.erl
index 51cb5419..e151ec7e 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_program_status.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_status.erl
@@ -7,7 +7,6 @@
-export([ allowed_methods/2
, options/2
, is_authorized/2
- , content_types_provided/2
, content_types_accepted/2
, resource_exists/2
]).
@@ -15,18 +14,16 @@
-export([ accept_status_update/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(program_status_opts, { user_id, program_id }).
+-record(state, { program_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
- UserId = cowboy_req:binding(user_id, Req),
ProgramId = cowboy_req:binding(program_id, Req),
{cowboy_rest, Req
- , #program_status_opts{ user_id=UserId
- , program_id=ProgramId
- }}.
+ , #state{ program_id=ProgramId }}.
resource_exists(Req, State) ->
case cowboy_req:method(Req) of
@@ -44,10 +41,9 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"POST">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
@@ -58,12 +54,14 @@ is_authorized(Req, State) ->
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
- #program_status_opts{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, {edit_program_status, ProgramId}) of
{true, UserId} ->
- { true, Req1, State };
- {true, _} -> %% Non matching user_id
- { { false, <<"Unauthorized to create a program here">>}, Req1, State };
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program) of
+ {ok, true} ->
+ { true, Req1, State };
+ {ok, false} ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end;
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
end
@@ -75,15 +73,12 @@ content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, []}, accept_status_update}],
Req, State}.
--spec accept_status_update(_, #program_status_opts{})
- -> {'false',_,#program_status_opts{}} | {'true',_,#program_status_opts{}}.
-accept_status_update(Req, #program_status_opts{user_id=UserId
- , program_id=ProgramId
- }) ->
- {ok, Body, _} = read_body(Req),
+-spec accept_status_update(_, #state{}) -> {boolean(),_,#state{}}.
+accept_status_update(Req, State=#state{program_id=ProgramId}) ->
+ {ok, Body, _} = ?UTILS:read_body(Req),
#{<<"enable">> := Status } = jiffy:decode(Body, [return_maps]),
- case automate_rest_api_backend:update_program_status(UserId, ProgramId, Status) of
+ case automate_bot_engine:change_program_status(ProgramId, Status) of
ok ->
Output = jiffy:encode(#{ <<"success">> => true
@@ -93,10 +88,7 @@ accept_status_update(Req, #program_status_opts{user_id=UserId
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
- { true, Res3, #program_status_opts{ user_id=UserId
- , program_id=ProgramId
- }
- };
+ { true, Res3, State };
{error, _} ->
Output = jiffy:encode(#{ <<"success">> => false
}),
@@ -105,22 +97,5 @@ accept_status_update(Req, #program_status_opts{user_id=UserId
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
- { false, Res3, #program_status_opts{ user_id=UserId
- , program_id=ProgramId
- }
- }
- end.
-
-%% GET handler
-content_types_provided(Req, State) ->
- {[{{<<"application">>, <<"json">>, []}, to_json}],
- Req, State}.
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
+ { false, Res3, State }
end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_stop.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_stop.erl
index c5c5cfa5..feabe8cc 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_program_stop.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_stop.erl
@@ -16,16 +16,13 @@
-include("./records.hrl").
--record(program_stop_thread_opts, { user_id, program_id }).
+-record(state, { program_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
- UserId = cowboy_req:binding(user_id, Req),
ProgramId = cowboy_req:binding(program_id, Req),
{cowboy_rest, Req
- , #program_stop_thread_opts{ user_id=UserId
- , program_id=ProgramId
- }}.
+ , #state{ program_id=ProgramId }}.
resource_exists(Req, State) ->
case cowboy_req:method(Req) of
@@ -43,10 +40,9 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods Program Stop Threads~n", []),
{[<<"POST">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
@@ -57,12 +53,14 @@ is_authorized(Req, State) ->
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
- #program_stop_thread_opts{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, edit_program_status) of
{true, UserId} ->
- { true, Req1, State };
- {true, _} -> %% Non matching user_id
- { { false, <<"Unauthorized to create a program here">>}, Req1, State };
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program) of
+ {ok, true} ->
+ { true, Req1, State };
+ {ok, false} ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end;
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
end
@@ -74,14 +72,10 @@ content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, []}, accept_thread_program_stop}],
Req, State}.
--spec accept_thread_program_stop(_, #program_stop_thread_opts{})
- -> {'false',_,#program_stop_thread_opts{}} | {'true',_,#program_stop_thread_opts{}}.
-accept_thread_program_stop(Req, #program_stop_thread_opts{user_id=UserId
- ,program_id=ProgramId
- }) ->
- {ok, _, _} = read_body(Req),
-
- case automate_rest_api_backend:stop_program_threads(UserId, ProgramId) of
+-spec accept_thread_program_stop(_, #state{})
+ -> {'false',_,#state{}} | {'true',_,#state{}}.
+accept_thread_program_stop(Req, State=#state{program_id=ProgramId}) ->
+ case automate_bot_engine:stop_program_threads(ProgramId) of
ok ->
Output = jiffy:encode(#{ <<"success">> => true
@@ -91,29 +85,5 @@ accept_thread_program_stop(Req, #program_stop_thread_opts{user_id=UserId
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
- { true, Res3, #program_stop_thread_opts{user_id=UserId
- ,program_id=ProgramId
- }
- };
- {error, _} ->
- Output = jiffy:encode(#{ <<"success">> => false
- }),
-
- Res1 = cowboy_req:set_resp_body(Output, Req),
- Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
- Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
-
- { false, Res3, #program_stop_thread_opts{ user_id=UserId
- , program_id=ProgramId
- }
- }
- end.
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
+ { true, Res3, State}
end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_tags.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_tags.erl
index b5dd2270..f2b06849 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_program_tags.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_tags.erl
@@ -16,18 +16,16 @@
, to_json/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(program_tag_opts, { user_id, program_id }).
+-record(state, { program_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
- UserId = cowboy_req:binding(user_id, Req),
ProgramId = cowboy_req:binding(program_id, Req),
{cowboy_rest, Req
- , #program_tag_opts{ user_id=UserId
- , program_id=ProgramId
- }}.
+ , #state{ program_id=ProgramId }}.
resource_exists(Req, State) ->
case cowboy_req:method(Req) of
@@ -45,26 +43,31 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+ Method ->
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
- #program_tag_opts{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ {Action, Scope} = case Method of
+ <<"GET">> -> {read_program, {read_program, ProgramId}};
+ _ -> {edit_program, {edit_program, ProgramId}}
+ end,
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
{true, UserId} ->
- { true, Req1, State };
- {true, _} -> %% Non matching user_id
- { { false, <<"Unauthorized to create a program here">>}, Req1, State };
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, Action) of
+ {ok, true} ->
+ { true, Req1, State };
+ {ok, false} ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end;
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
end
@@ -76,15 +79,12 @@ content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, []}, accept_tags_update}],
Req, State}.
--spec accept_tags_update(_, #program_tag_opts{})
- -> {'false',_,#program_tag_opts{}} | {'true',_,#program_tag_opts{}}.
-accept_tags_update(Req, #program_tag_opts{user_id=UserId
- , program_id=ProgramId
- }) ->
- {ok, Body, _} = read_body(Req),
+-spec accept_tags_update(_, #state{}) -> {boolean(),_,#state{}}.
+accept_tags_update(Req, State=#state{program_id=ProgramId }) ->
+ {ok, Body, _} = ?UTILS:read_body(Req),
#{<<"tags">> := Tags } = jiffy:decode(Body, [return_maps]),
- case automate_rest_api_backend:update_program_tags(UserId, ProgramId, Tags) of
+ case automate_storage:register_program_tags(ProgramId, Tags) of
ok ->
Output = jiffy:encode(#{ <<"success">> => true
@@ -94,10 +94,7 @@ accept_tags_update(Req, #program_tag_opts{user_id=UserId
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
- { true, Res3, #program_tag_opts{ user_id=UserId
- , program_id=ProgramId
- }
- };
+ { true, Res3, State };
{error, _} ->
Output = jiffy:encode(#{ <<"success">> => false
}),
@@ -106,10 +103,7 @@ accept_tags_update(Req, #program_tag_opts{user_id=UserId
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
- { false, Res3, #program_tag_opts{ user_id=UserId
- , program_id=ProgramId
- }
- }
+ { false, Res3, State }
end.
%% GET handler
@@ -117,11 +111,9 @@ content_types_provided(Req, State) ->
{[{{<<"application">>, <<"json">>, []}, to_json}],
Req, State}.
--spec to_json(cowboy_req:req(), #program_tag_opts{})
- -> {binary(),cowboy_req:req(), #program_tag_opts{}}.
-to_json(Req, State) ->
- #program_tag_opts{user_id=UserId, program_id=ProgramId} = State,
- case automate_rest_api_backend:get_program_tags(UserId, ProgramId) of
+-spec to_json(cowboy_req:req(), #state{}) -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{program_id=ProgramId}) ->
+ case automate_storage:get_tags_program_from_id(ProgramId) of
{ ok, Tags } ->
Output = jiffy:encode(Tags),
Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
@@ -129,13 +121,3 @@ to_json(Req, State) ->
{ Output, Res2, State }
end.
-
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_variables_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_variables_root.erl
new file mode 100644
index 00000000..f7c5948a
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_variables_root.erl
@@ -0,0 +1,121 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_variables_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ , content_types_accepted/2
+ ]).
+
+-export([ to_json/2
+ , accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { program_id :: binary() }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"PATCH">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ Method ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ Scope = case Method of
+ <<"GET">> -> {read_program_variables, ProgramId};
+ <<"PATCH">> -> {edit_program_variables, ProgramId}
+ end,
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UserId} ->
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program) of
+ {ok, true} ->
+ { true, Req1, State };
+ {ok, false} ->
+ { { false, <<"Not authorized">> }, Req1, State}
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State) ->
+ #state{ program_id=ProgramId} = State,
+ case automate_storage:get_program_variables(ProgramId) of
+ { ok, VariableMap } ->
+ Output = jiffy:encode(#{ variables => ?FORMATTING:serialize_variable_map(VariableMap)}),
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+
+%% PATCH handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+accept_json(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"PATCH">> ->
+ update_variables(Req, State)
+ end.
+
+
+update_variables(Req, State=#state{program_id=ProgramId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ #{ <<"values">> := Values } = jiffy:decode(Body, [return_maps]),
+ %% Wrap all in the same transaction
+ {atomic, ok} = mnesia:transaction(fun() ->
+ ok = lists:foreach(fun(#{ <<"name">> := Name, <<"value">> := Value}) ->
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, Name, Value, undefined)
+ end, Values)
+ end),
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true }), Req1),
+ { true, Req2, State }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_program_variables_specific.erl b/backend/apps/automate_rest_api/src/automate_rest_api_program_variables_specific.erl
new file mode 100644
index 00000000..e82d1042
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_program_variables_specific.erl
@@ -0,0 +1,86 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_program_variables_specific).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , resource_exists/2
+ , delete_resource/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { program_id :: binary()
+ , var_name :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ VarName = cowboy_req:binding(var_name, Req),
+
+ {cowboy_rest, Req
+ , #state{ program_id=ProgramId
+ , var_name=VarName
+ }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"DELETE">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, { edit_program_variables, ProgramId }) of
+ {true, UserId} ->
+ case automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program) of
+ {ok, true} ->
+ { true, Req1, State };
+ {ok, false} ->
+ { { false, <<"Not authorized">> }, Req1, State}
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+
+%% DELETE handler
+delete_resource(Req, State=#state{program_id=ProgramId, var_name=VarName}) ->
+ case automate_bot_engine_variables:delete_program_variable(ProgramId, VarName) of
+ ok ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
+ { true, Req1, State };
+ { error, Reason } ->
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req),
+ { false, Req1, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_programs_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_programs_root.erl
index 28fffe74..d02da3e4 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_programs_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_programs_root.erl
@@ -17,8 +17,11 @@
]).
-include("./records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(PROGRAMS, automate_rest_api_utils_programs).
--record(create_program_seq, { username }).
+-record(create_program_seq, { username :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -42,7 +45,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -51,13 +53,18 @@ is_authorized(Req, State) ->
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+ Method ->
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
+ Scope = case Method of
+ <<"GET">> -> list_programs;
+ <<"POST">> -> create_programs
+ end,
+
#create_program_seq{username=Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ case automate_rest_api_backend:is_valid_token(X, Scope) of
{true, Username} ->
{ true, Req1, State };
{true, _} -> %% Non matching username
@@ -77,12 +84,16 @@ content_types_accepted(Req, State) ->
-> {{'true', binary()},cowboy_req:req(), #create_program_seq{}}.
accept_json_create_program(Req, State) ->
#create_program_seq{username=Username} = State,
- case automate_rest_api_backend:create_program(Username) of
- { ok, {ProgramId, ProgramName, ProgramUrl} } ->
+
+ {ok, Body, _} = ?UTILS:read_body(Req),
+ {Type, Name} = ?PROGRAMS:get_metadata_from_body(Body),
+ case automate_rest_api_backend:create_program(Username, Name, Type) of
+ { ok, {ProgramId, ProgramName, ProgramUrl, ProgramType} } ->
Output = jiffy:encode(#{ <<"id">> => ProgramId
, <<"name">> => ProgramName
, <<"link">> => ProgramUrl
+ , <<"type">> => ProgramType
}),
Res1 = cowboy_req:set_resp_body(Output, Req),
@@ -103,7 +114,6 @@ to_json(Req, State) ->
#create_program_seq{username=Username} = State,
case automate_rest_api_backend:lists_programs_from_username(Username) of
{ ok, Programs } ->
-
Output = jiffy:encode(encode_program_list(Programs)),
Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
@@ -113,21 +123,12 @@ to_json(Req, State) ->
encode_program_list(Programs) ->
- encode_program_list(Programs, []).
-
-
-encode_program_list([], Acc) ->
- lists:reverse(Acc);
-
-encode_program_list([H | T], Acc) ->
- #program_metadata{ id=Id
- , name=Name
- , link=Link
- , enabled=Enabled
- } = H,
- AsDictionary = #{ <<"id">> => Id
- , <<"name">> => Name
- , <<"link">> => Link
- , <<"enabled">> => Enabled
- },
- encode_program_list(T, [AsDictionary | Acc]).
+ lists:map(fun(Program=#program_metadata{id=Id}) ->
+ ProgramBridges = try ?UTILS:get_bridges_on_program_id(Id) of
+ Bridges -> Bridges
+ catch ErrNS:Error:StackTrace ->
+ automate_logging:log_platform(error, ErrNS, Error, StackTrace),
+ []
+ end,
+ ?FORMATTING:program_listing_to_json(Program, ProgramBridges)
+ end, Programs).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_programs_specific.erl b/backend/apps/automate_rest_api/src/automate_rest_api_programs_specific.erl
index 824bb247..f3054282 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_programs_specific.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_programs_specific.erl
@@ -18,18 +18,27 @@
-include("./records.hrl").
-include("../../automate_storage/src/records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
--record(get_program_seq, { username, program_name }).
+-record(state, { username :: binary()
+ , program_name :: binary()
+ , program_id :: binary()
+ , user_id :: undefined | binary()
+ }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
- UserId = cowboy_req:binding(user_id, Req),
+ UserName = cowboy_req:binding(user_id, Req),
ProgramName = cowboy_req:binding(program_id, Req),
Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, #user_program_entry{ id=ProgramId }} = automate_storage:get_program(UserName, ProgramName),
{cowboy_rest, Req1
- , #get_program_seq{ username=UserId
- , program_name=ProgramName
- }}.
+ , #state{ username=UserName
+ , program_name=ProgramName
+ , program_id=ProgramId
+ , user_id=undefined
+ }}.
%% CORS
options(Req, State) ->
@@ -38,26 +47,52 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[SPProgram]Asking for methods~n", []),
{[<<"GET">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{username=Username, program_id=ProgramId}) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+
+ Method ->
+ {ok, #user_program_entry{ visibility=Visibility }} = automate_storage:get_program_from_id(ProgramId),
+ IsPublic = ?UTILS:is_public(Visibility),
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
- { {false, <<"Authorization header not found">>} , Req1, State };
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ { true, Req1, State };
+ _ ->
+ { {false, <<"Authorization header not found">>} , Req1, State }
+ end;
X ->
- #get_program_seq{username=Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ {Action, Scope} = case Method of
+ <<"GET">> -> {read_program, { read_program, ProgramId }};
+ <<"PUT">> -> {edit_program, { edit_program, ProgramId }};
+ <<"PATCH">> -> {edit_program, { edit_program_metadata, ProgramId }};
+ <<"DELETE">> -> {delete_program, { delete_program, ProgramId }}
+ end,
+ case automate_rest_api_backend:is_valid_token(X, Scope) of
{true, Username} ->
- { true, Req1, State };
- {true, _} -> %% Non matching username
- { { false, <<"Unauthorized to create a program here">>}, Req1, State };
+ {ok, {user, UId}} = automate_storage:get_userid_from_username(Username),
+ case automate_storage:is_user_allowed({user, UId}, ProgramId, Action) of
+ {ok, true} ->
+ { true, Req1, State#state{user_id=UId} };
+ {ok, false} ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ {true, Req1, State#state{user_id=UId}};
+ _ ->
+ { { false, <<"Action not authorized">>}, Req1, State }
+ end;
+ {error, Reason} ->
+ automate_logging:log_api(warning, ?MODULE, {authorization_error, Reason}),
+ { { false, <<"Error on authorization">>}, Req1, State }
+ end;
+ {true, AuthUser} -> %% Non matching username
+ { { false, <<"Authorization not correct">>}, Req1, State };
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
end
@@ -66,46 +101,64 @@ is_authorized(Req, State) ->
%% Get handler
content_types_provided(Req, State) ->
- io:fwrite("User > program > ID~n", []),
{[{{<<"application">>, <<"json">>, []}, to_json}],
Req, State}.
--spec to_json(cowboy_req:req(), #get_program_seq{})
- -> {binary(),cowboy_req:req(), #get_program_seq{}}.
-to_json(Req, State) ->
- #get_program_seq{username=Username, program_name=ProgramName} = State,
- case automate_rest_api_backend:get_program(Username, ProgramName) of
- { ok, Program } ->
- io:fwrite("PROGRAM: ~p~n", [Program]),
- Output = program_to_json(Program),
+-spec to_json(cowboy_req:req(), #state{})
+ -> { stop | binary() ,cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{program_id=ProgramId, user_id=UserId}) ->
+ Qs = cowboy_req:parse_qs(Req),
+ IncludePages = case proplists:get_value(<<"retrieve_pages">>, Qs) of
+ <<"yes">> ->
+ true;
+ _ ->
+ false
+ end,
+
+ case automate_rest_api_backend:get_program(ProgramId) of
+ { ok, Program=#user_program{ id=ProgramId, last_upload_time=ProgramTime } } ->
+ Checkpoint = case automate_storage:get_last_checkpoint_content(ProgramId) of
+ {ok, #user_program_checkpoint{event_time=CheckpointTime, content=Content} } ->
+ case ProgramTime < (CheckpointTime / 1000) of
+ true ->
+ Content;
+ false ->
+ null
+ end;
+ {error, not_found} ->
+ null
+ end,
+ Json = ?FORMATTING:program_data_to_json(Program, Checkpoint),
+
+ {ok, CanEdit} = automate_storage:is_user_allowed({user, UserId}, ProgramId, edit_program),
+ {ok, CanAdmin } = automate_storage:is_user_allowed({user, UserId}, ProgramId, admin_program),
+
+ Json2 = Json#{ readonly => not CanEdit, can_admin => CanAdmin },
+ Json3 = case IncludePages of
+ false -> Json2;
+ true ->
+ {ok, Pages} = automate_storage:get_program_pages(ProgramId),
+ Json#{ pages => maps:from_list(lists:map(fun (#program_pages_entry{ page_id={_, Path}
+ , contents=Contents}) ->
+ {Path, Contents}
+ end, Pages))
+ }
+ end,
Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
- { Output, Res2, State }
+ { jiffy:encode(Json3), Res2, State };
+ {error, Reason} ->
+ Code = 500,
+ Output = jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }),
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
end.
-program_to_json(#user_program{ id=Id
- , user_id=UserId
- , program_name=ProgramName
- , program_type=ProgramType
- , program_parsed=ProgramParsed
- , program_orig=ProgramOrig
- , enabled=Enabled
- }) ->
- jiffy:encode(#{ <<"id">> => Id
- , <<"owner">> => UserId
- , <<"name">> => ProgramName
- , <<"type">> => ProgramType
- , <<"parsed">> => ProgramParsed
- , <<"orig">> => ProgramOrig
- , <<"enabled">> => Enabled
- }).
-
content_types_accepted(Req, State) ->
- io:fwrite("[PUT] User > program > ID~n", []),
{[{{<<"application">>, <<"json">>, []}, accept_json_program}],
Req, State}.
@@ -119,10 +172,10 @@ accept_json_program(Req, State) ->
%% PUT handler
update_program(Req, State) ->
- #get_program_seq{program_name=ProgramName, username=Username} = State,
+ #state{program_name=ProgramName, username=Username} = State,
- {ok, Body, Req1} = read_body(Req),
- Parsed = [jiffy:decode(Body, [return_maps])],
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
Program = decode_program(Parsed),
case automate_rest_api_backend:update_program(Username, ProgramName, Program) of
ok ->
@@ -135,12 +188,11 @@ update_program(Req, State) ->
%% PATCH handler
update_program_metadata(Req, State) ->
- #get_program_seq{program_name=ProgramName, username=Username} = State,
+ #state{program_name=ProgramName, username=Username} = State,
- {ok, Body, Req1} = read_body(Req),
- Parsed = [jiffy:decode(Body, [return_maps])],
- Metadata = decode_program_metadata(Parsed),
- case automate_rest_api_backend:update_program_metadata(Username, ProgramName, Metadata) of
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ Parsed = jiffy:decode(Body, [return_maps]),
+ case automate_rest_api_backend:update_program_metadata(Username, ProgramName, Parsed) of
{ok, #{ <<"link">> := Link } } ->
Req2 = send_json_output(jiffy:encode(#{ <<"success">> => true, <<"link">> => Link}), Req),
{ true, Req2, State };
@@ -151,7 +203,7 @@ update_program_metadata(Req, State) ->
%% DELETE handler
delete_resource(Req, State) ->
- #get_program_seq{program_name=ProgramName, username=Username} = State,
+ #state{program_name=ProgramName, username=Username} = State,
case automate_rest_api_backend:delete_program(Username, ProgramName) of
ok ->
Req1 = send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
@@ -163,19 +215,17 @@ delete_resource(Req, State) ->
%% Converters
-decode_program_metadata([#{ <<"name">> := ProgramName
- }]) ->
- #editable_user_program_metadata { program_name=ProgramName
- }.
-
-
-decode_program([#{ <<"type">> := ProgramType
- , <<"orig">> := ProgramOrig
- , <<"parsed">> := ProgramParsed
- }]) ->
+decode_program(P=#{ <<"type">> := ProgramType
+ , <<"orig">> := ProgramOrig
+ , <<"parsed">> := ProgramParsed
+ }) ->
#program_content { type=ProgramType
, orig=ProgramOrig
, parsed=ProgramParsed
+ , pages=case P of
+ #{ <<"pages">> := Pages} -> Pages;
+ _ -> #{}
+ end
}.
@@ -183,13 +233,3 @@ send_json_output(Output, Req) ->
Res1 = cowboy_req:set_resp_body(Output, Req),
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2).
-
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_renderer.erl b/backend/apps/automate_rest_api/src/automate_rest_api_renderer.erl
new file mode 100644
index 00000000..ca35ed51
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_renderer.erl
@@ -0,0 +1,514 @@
+-module(automate_rest_api_renderer).
+
+-export([ render_page/4
+ ]).
+
+-define(DEFAULT_TITLE, <<"Page title">>).
+-define(DEFAULT_IMAGE_WIDTH, <<"100">>).
+-define(DEFAULT_IMAGE_HEIGHT, <<"100">>).
+
+%%====================================================================
+%% API functions
+%%====================================================================
+-spec render_page(binary(), _, cowboy_req:req(), page | element) -> iolist().
+render_page(ProgramId, Page, Req, RenderAs) ->
+ {ok, Values} = automate_storage:get_widget_values_in_program(ProgramId),
+ [ render_page_header(Page, RenderAs)
+ , render_page_body(Page, ProgramId, Values, Req)
+ , render_page_footer(ProgramId, Page, RenderAs)
+ ].
+
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+render_page_header(Page, page) ->
+ [ <<"\n">>
+ , <<" \n">>
+ , <<" ">>
+ , <<"">>, html_escape(maps:get(<<"title">>, Page, ?DEFAULT_TITLE)), <<" ">>
+ , render_styles(page)
+ , <<"\n\n">>
+ ];
+render_page_header(_Page, element) ->
+ [ ""
+ , render_styles(element)
+ ].
+
+render_page_body(#{ <<"value">> := Contents }, ProgramId, Values, Req) ->
+ render_element(Contents, ProgramId, Values, Req).
+
+render_page_footer(ProgramId, #{ <<"value">> := Contents }, page) ->
+ [ render_scripts(ProgramId, Contents)
+ , <<"\n">>
+ ];
+
+render_page_footer(ProgramId, #{ <<"value">> := Contents }, element) ->
+ [ render_scripts(ProgramId, Contents)
+ , "\n
"
+ ].
+
+raw_to_html(Bin) when is_binary(Bin) ->
+ html_escape(Bin);
+raw_to_html([Contained]) ->
+ raw_to_html(Contained);
+raw_to_html(X) ->
+ raw_to_html(list_to_binary(io_lib:format("~w", [X]))).
+
+html_escape(Str) ->
+ Lines = binary:split(Str, <<"\n">>, [global]),
+ EscapedLines = lists:map(fun mochiweb_html:escape_attr/1, Lines),
+ lists:join(" ", EscapedLines).
+
+%%====================================================================
+%% Element rendering
+%%====================================================================
+render_element(null, _ProgramId, _Values, _Req) ->
+ [<<"🚧 Work in progress 🚧
">>
+ ];
+
+render_element(E=#{ <<"cut_type">> := CutType
+ , <<"groups">> := Groups
+ }, ProgramId, Values, Req) ->
+
+ ElementBackground = case E of
+ #{ <<"settings">> := #{ <<"bg">> := #{ <<"type">> := <<"color">>
+ , <<"value">> := Color
+ }}} ->
+ [ "background-color:"
+ , Color %% TODO: Validate that the color is a correct one.
+ ];
+ _ -> []
+ end,
+
+ [ <<">
+ , "style='", ElementBackground, "' "
+ , ">"
+ , "
"
+ , lists:map(fun(El) -> render_element(El, ProgramId, Values, Req) end, Groups)
+ , <<"
">>
+ ];
+
+render_element(E=#{ <<"container_type">> := <<"simple_card">>
+ , <<"content">> := Content
+ }, ProgramId, Values, Req) ->
+ ElementBackground = case E of
+ #{ <<"settings">> := #{ <<"bg">> := #{ <<"type">> := <<"color">>
+ , <<"value">> := Color
+ }}} ->
+ [ "background-color:"
+ , Color %% TODO: Validate that the color is a correct one.
+ ];
+ _ -> []
+ end,
+
+ [ ""
+ , "
"
+ , case Content of
+ null -> "";
+ _ -> render_element(Content, ProgramId, Values, Req)
+ end
+ , <<"
">>
+ ];
+
+render_element(E=#{ <<"container_type">> := <<"link_area">>
+ , <<"content">> := Content
+ }, ProgramId, Values, Req) ->
+ Target = case E of
+ #{ <<"settings">> := #{ <<"target">> := #{ <<"link">> := #{ <<"value">> := Link }
+ }}} ->
+ Link; %% TODO: Validate link types
+ _ -> "#"
+ end,
+ OpenInTab = case E of
+ #{ <<"settings">> := #{ <<"target">> := #{ <<"openInTab">> := #{ <<"value">> := DoOpenInTab }
+ }}} when is_boolean(DoOpenInTab) ->
+ DoOpenInTab;
+ _ -> false
+ end,
+
+ [ " " target='_blank'"; _ -> "" end
+ , ">"
+ , ""
+ , case Content of
+ null -> "";
+ _ -> render_element(Content, ProgramId, Values, Req)
+ end
+ , <<"
">>
+ ];
+
+render_element(E=#{ <<"widget_type">> := <<"simple_button">>
+ , <<"id">> := WidgetId
+ }, _ProgramId, _Values, _Req) ->
+ [ <<"">>
+ , html_escape(maps:get(<<"text">>, E, "Click me!"))
+ , <<"
">>
+ ];
+
+render_element(E=#{ <<"widget_type">> := <<"fixed_text">>
+ , <<"id">> := WidgetId
+ }, _ProgramId, _Values, _Req) ->
+ ElementStyle = get_text_element_style(E),
+ [ <<"">>
+ ];
+
+render_element(E=#{ <<"widget_type">> := Type= <<"text_box">>
+ , <<"id">> := WidgetId
+ }, _ProgramId, Values, _Req) ->
+ Contents = raw_to_html(maps:get(<<"text">>, E,
+ maps:get(<>, Values,
+ <<"">>))),
+ ElementStyle = get_text_element_style(E),
+ [ <<" >
+ , " style='", ElementStyle, "'"
+ , <<" placeholder=\"">>
+ , Contents
+ , <<"\" />
">>
+ ];
+
+render_element(E=#{ <<"widget_type">> := Type= <<"dynamic_text">>
+ , <<"id">> := WidgetId
+ }, _ProgramId, Values, _Req) ->
+ Contents = raw_to_html(maps:get(<<"text">>, E,
+ maps:get(<>, Values,
+ <<"- No content yet -">>))),
+ ElementStyle = get_text_element_style(E),
+ [ <<"">>
+ ];
+
+
+render_element(E=#{ <<"widget_type">> := <<"fixed_image">>
+ , <<"id">> := _WidgetId
+ }, ProgramId, _Values, Req) ->
+ ImgUrl = get_image_url(E, ProgramId, Req),
+ Dimensions = get_image_dimensions(E),
+ [ <<"">>
+ ];
+
+render_element(E=#{ <<"widget_type">> := <<"horizontal_separator">>
+ , <<"id">> := _WidgetId
+ }, _ProgramId, _Values, _Req) ->
+ [ " > := #{ <<"body">> := #{ <<"widthTaken">> := #{ <<"value">> := Width } } } } ->
+ [ "class='size-", Width, "' " ];
+ _ ->
+ []
+ end
+ , "/>"
+ ].
+
+
+%%====================================================================
+%% Auxiliary sections
+%%====================================================================
+render_styles(RenderAs) ->
+ MaterialShadow = "0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)",
+
+ {Root, RootStyle} = case RenderAs of
+ page -> { "",
+ [ "body { height: 100vh; text-align: center; } "
+ , " body {"
+ , "font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;"
+ , "font-size: 1rem;"
+ , "font-weight: 400;"
+ , "line-height: 1.5;"
+ , "color: #212529;"
+ , "text-align: left; }"
+ , " * { margin: 0; padding: 0; box-sizing: border-box; } "
+ ]};
+ element ->
+ RootEl = "div.programaker-element ",
+ { RootEl,
+ [ RootEl, "* { margin: 0; padding: 0; box-sizing: border-box; } "
+ , RootEl, "{ text-align: center; } "
+ ]}
+ end,
+
+ [ <<"">>
+ ].
+
+render_scripts(ProgramId, Contents) ->
+ ScriptContents = wire_components(Contents),
+ [ <<"">>
+ ].
+
+wire_components(null) ->
+ [];
+wire_components(#{ <<"widget_type">> := <<"fixed_text">>
+ }) ->
+ [];
+wire_components(#{ <<"widget_type">> := <<"fixed_image">>
+ }) ->
+ [];
+wire_components(#{ <<"widget_type">> := <<"horizontal_separator">>
+ }) ->
+ [];
+wire_components(#{ <<"container_type">> := _
+ , <<"content">> := Content
+ }) ->
+ wire_components(Content);
+
+wire_components(#{ <<"cut_type">> := _CutType
+ , <<"groups">> := Groups
+ }) ->
+ lists:map(fun wire_components/1, Groups);
+
+wire_components(#{ <<"widget_type">> := <<"text_box">>
+ , <<"id">> := WidgetId
+ }) ->
+ [ "addCollectable('", WidgetId, "', function() { return document.getElementById('elem-", WidgetId, "').value });"
+ , "document.getElementById('elem-", WidgetId ,"').onkeyup = (function() {\n"
+ , "websocket.send(JSON.stringify({\n"
+ , " type: 'ui-event',\n"
+ , " value: {\n"
+ , " action: 'changed',\n"
+ , " block_type: 'text_box',\n"
+ , " block_id: '", WidgetId, "',\n"
+ , " data: collectData(),\n"
+ , "}}));\n"
+ , "});\n"
+ ];
+
+
+wire_components(#{ <<"widget_type">> := <<"simple_button">>
+ , <<"id">> := WidgetId
+ }) ->
+ [ "addCollectable('", WidgetId, "', function() { return document.getElementById('elem-", WidgetId, "').innerText });"
+ , "document.getElementById('elem-", WidgetId ,"').onclick = (function() {\n"
+ , "websocket.send(JSON.stringify({\n"
+ , " type: 'ui-event',\n"
+ , " value: {\n"
+ , " action: 'activated',\n"
+ , " block_type: 'simple_button',\n"
+ , " block_id: '", WidgetId, "',\n"
+ , " data: collectData(),\n"
+ , "}}));\n"
+ , "});\n"
+ ];
+
+wire_components(#{ <<"widget_type">> := <<"dynamic_text">>
+ , <<"id">> := WidgetId
+ }) ->
+ [ "link_widget('", WidgetId, "');"].
+
+render_connection_block_start(ProgramId) ->
+ Url = ["/api/v0/programs/by-id/", ProgramId, "/ui-events"],
+ [ "\n(function(){ \n"
+ , "var listeners = {};"
+ , "var link_widget = (function(id){ listeners[id] = (function(data){var e = document.getElementById('elem-' + id); e.innerText = data.values[0];}); });"
+ , "var dispatch = (function(data) { \n"
+ , " var id = data.subkey.split('.')[1]; if (listeners[id]) { listeners[id](data); } \n"
+ , " else { console.warn('Received event for unknown widget:', data.subkey); } });\n"
+ , "var ws_url = (document.location.origin + '", Url, "').replace(/^http/, 'ws');\n"
+ , "console.log('Connecting to websocket on', ws_url);\n"
+ , "var websocket = new WebSocket(ws_url);\n"
+ , "websocket.onmessage = (function(ev) {\n"
+ , " var parsed = JSON.parse(ev.data);\n"
+ , " dispatch(parsed);\n"
+ , "});\n"
+
+ , "websocket.onclose = (function() { console.error('Connection closed'); });\n"
+ , "websocket.onerror = (function(err) { console.error(err); });\n"
+ , "websocket.onopen = (function() { console.log('Connection opened'); \n"
+ , " websocket.send(JSON.stringify({ type: 'AUTHENTICATION', value: { token: 'ANONYMOUS' }}));\n"
+ , "});\n"
+ , "var collectables = [];\n"
+ , "var addCollectable = (function(id, item) { collectables.push([id, item]) });\n"
+ , "var collectData = (function() { var result = {}; for (var coll of collectables) { result[coll[0]] = coll[1](); }; return result; } );\n"
+ ].
+
+render_connection_block_end() ->
+ <<"})();">>.
+
+%% Element attribute management
+get_text_element_style(E) ->
+ [ get_text_element_font_size_style(E)
+ , get_text_element_font_weight_style(E)
+ , get_text_element_text_color_style(E)
+ , get_text_element_background_color_style(E)
+ , get_text_element_underline_color_style(E)
+ ].
+
+get_text_element_text_color_style(#{ <<"settings">> := #{ <<"text">> := #{ <<"color">> := #{ <<"value">> := Value } } } }) ->
+ [ "color: ", Value, ";"
+ ];
+get_text_element_text_color_style(_) ->
+ [].
+
+get_text_element_font_size_style(#{ <<"settings">> := #{ <<"text">> := #{ <<"fontSize">> := #{ <<"value">> := Value } } } }) ->
+ [ "font-size: ", responsive_font_size(Value), ";"
+ ];
+get_text_element_font_size_style(_) ->
+ [].
+
+get_text_element_font_weight_style(#{ <<"settings">> := #{ <<"text">> := #{ <<"fontWeight">> := #{ <<"value">> := Value } } } }) ->
+ [ "font-weight: ", font_weight_to_css(Value), ";"
+ ];
+get_text_element_font_weight_style(_) ->
+ [].
+
+get_text_element_background_color_style(#{ <<"settings">> := #{ <<"bg">> := #{ <<"type">> := <<"color">>
+ , <<"value">> := Value } } }) ->
+ [ "background-color: ", Value, ";"
+ ];
+get_text_element_background_color_style(#{ <<"settings">> := #{ <<"bg">> := #{ <<"type">> := <<"transparent">> } } }) ->
+ "background: transparent;";
+get_text_element_background_color_style(_) ->
+ [].
+
+get_image_url(#{ <<"settings">> := #{ <<"body">> := #{ <<"image">> := #{ <<"id">> := ImgId } } } }, ProgramId, Req) ->
+ ImagePath = [ "/api/v0/programs/by-id/"
+ , ProgramId
+ , "/assets/by-id/"
+ , ImgId
+ ],
+ BaseChanges = #{path => ImagePath, qs => undefined },
+ Changes = case automate_configuration:get_backend_api_info() of
+ undefined ->
+ BaseChanges;
+ BackendInfo ->
+ maps:merge(BackendInfo, BaseChanges)
+ end,
+ cowboy_req:uri(Req, Changes);
+
+get_image_url(_, _, _) ->
+ []. %% TODO: Add a default image?
+
+get_image_dimensions(#{ <<"dimensions">> := #{ <<"width">> := Width, <<"height">> := Height } }) ->
+ ["width='", num_to_binary(Width),"' height='", num_to_binary(Height), "'"];
+get_image_dimensions(_) ->
+ ["width='", ?DEFAULT_IMAGE_WIDTH,"' height='", ?DEFAULT_IMAGE_HEIGHT, "'"].
+
+get_text_element_underline_color_style(#{ <<"underline">> := <<"none">>}) ->
+ "text-decoration: none;";
+get_text_element_underline_color_style(#{ <<"underline">> := #{ <<"color">> := Color}}) ->
+ ["text-decoration: underline; text-decoration-color: ", Color, ";"];
+get_text_element_underline_color_style(#{ <<"underline">> := <<"default">>}) ->
+ "";
+get_text_element_underline_color_style(_) ->
+ "".
+
+
+get_fixed_text_content(#{ <<"content">> := Content }) ->
+ formatted_text_to_html(Content);
+get_fixed_text_content(E) ->
+ html_escape(maps:get(<<"text">>, E, "Right click me to edit this text!")).
+
+formatted_text_to_html(FT) ->
+ lists:map(fun formatted_element_to_html/1, FT).
+
+formatted_element_to_html(#{ <<"type">> := <<"text">>
+ , <<"value">> := Text
+ }) ->
+ html_escape(Text);
+formatted_element_to_html(E=#{ <<"type">> := <<"link">>
+ , <<"target">> := Target
+ , <<"contents">> := Contents
+ , <<"open_in_tab">> := OpenInTab
+ }) ->
+ [ " " target='_blank'"; _ -> "" end
+ , " style='", get_text_element_style(E), "'"
+ , ">"
+ , formatted_text_to_html(Contents)
+ , " "
+ ];
+formatted_element_to_html(#{ <<"type">> := <<"text-color">>
+ , <<"color">> := Color
+ , <<"contents">> := Contents
+ }) ->
+ [ ""
+ , formatted_text_to_html(Contents)
+ , " "
+ ];
+formatted_element_to_html(#{ <<"type">> := <<"format">>
+ , <<"format">> := Format
+ , <<"contents">> := Contents
+ }) ->
+ Tag = format_to_tag(Format),
+ [ "<", Tag, ">"
+ , formatted_text_to_html(Contents)
+ , "", Tag, ">"
+ ].
+
+format_to_tag(<<"bold">>) ->
+ "b";
+format_to_tag(<<"italic">>) ->
+ "i";
+format_to_tag(<<"underline">>) ->
+ "u".
+
+
+num_to_binary(X) when is_float(X) ->
+%% This is needed to avoid scientific notation, which can cause problems on CSS
+ list_to_binary(io_lib:format("~p", [X]));
+num_to_binary(X) when is_integer(X) ->
+ integer_to_binary(X).
+
+%% Attribute translation
+font_weight_to_css(<<"normal">>) ->
+ "normal";
+font_weight_to_css(<<"bold">>) ->
+ "bold";
+font_weight_to_css(<<"light">>) ->
+ "300";
+font_weight_to_css(<<"super-light">>) ->
+ "100";
+font_weight_to_css(<<"super-bold">>) ->
+ "900".
+
+%% Set limits to font size
+responsive_font_size(FontSizeInPx) ->
+ MinSize = "0.5rem",
+ MaxSize = "10vw",
+ [ "clamp(", MinSize
+ , ", ", integer_to_binary(FontSizeInPx), "px"
+ , ", ", MaxSize, ")"
+ ].
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_server.erl b/backend/apps/automate_rest_api/src/automate_rest_api_server.erl
index 857cdca9..f6e3c83a 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_server.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_server.erl
@@ -18,50 +18,174 @@ start_link() ->
Dispatch = cowboy_router:compile(
[{'_', [ %% Metrics
{"/metrics", automate_rest_api_metrics, []}
+ , {"/api/v0/ping", automate_rest_api_ping, []}
+
+ %% Internal
+ , {"/internal/validate_connection_token/program/by-id/:program_id", automate_rest_api_validate_connection_token_by_program_id, []}
+
+ %% Administration
+ , {"/api/v0/admin/stats", automate_rest_api_admin_stats_root, []}
+
+ %% Assets
+ , {"/api/v0/assets/icons/[...]", cowboy_static, {dir, automate_configuration:asset_directory("public/icons")}}
- %% API
+ %% Registration
, {"/api/v0/sessions/register", automate_rest_api_sessions_register, []}
, {"/api/v0/sessions/register/verify", automate_rest_api_sessions_register_verify, []}
- , {"/api/v0/sessions/check", automate_rest_api_sessions_check, []}
- , {"/api/v0/sessions/login", automate_rest_api_sessions_login, []}
, {"/api/v0/sessions/login/reset", automate_rest_api_sessions_reset_password, []}
, {"/api/v0/sessions/login/reset/validate", automate_rest_api_sessions_reset_password_validate, []}
, {"/api/v0/sessions/login/reset/update", automate_rest_api_sessions_reset_password_update, []}
- , {"/api/v0/ping", automate_rest_api_ping, []}
+ %% Session management
+ , {"/api/v0/sessions/check", automate_rest_api_sessions_check, []}
+ , {"/api/v0/sessions/login", automate_rest_api_sessions_login, []}
+ , {"/api/v0/tokens", automate_rest_api_tokens_root, []}
+ %% Users
, {"/api/v0/users", automate_rest_api_users_root, []}
, {"/api/v0/users/:user_id", automate_rest_api_users_specific, []}
+ , {"/api/v0/users/by-id/:user_id/picture", automate_rest_api_users_picture, []}
+ , {"/api/v0/users/by-id/:user_id/assets", automate_rest_api_user_assets, [user]}
+ , {"/api/v0/users/by-id/:user_id/assets/by-id/:asset_id", automate_rest_api_user_asset_by_id, [user]}
+ %% Miscellaneous
, {"/api/v0/users/id/:user_id/custom_signals/", automate_rest_api_custom_signals_root, []}
+ , {"/api/v0/users/id/:user_id/groups/", automate_rest_api_user_groups_root, []}
, {"/api/v0/users/id/:user_id/templates/", automate_rest_api_templates_root, []}
, {"/api/v0/users/id/:user_id/templates/id/:template_id", automate_rest_api_templates_specific, []}
, {"/api/v0/users/:user_id/custom-blocks/", automate_rest_api_custom_blocks_root, []}
+ , {"/api/v0/users/by-name/:user_name/profile", automate_rest_api_user_profile_by_name, [] }
+
+
+ %% Settings
+ , {"/api/v0/users/id/:user_id/settings", automate_rest_api_user_settings, []}
+ , {"/api/v0/users/id/:user_id/profile", automate_rest_api_user_by_id_profile, []}
+
+ %% Programs
+ , {"/api/v0/programs/id/:program_id", automate_rest_api_program_specific_by_id, []} %% DUP with /by-id/ form
+
, {"/api/v0/users/:user_id/programs", automate_rest_api_programs_root, []}
, {"/api/v0/users/:user_id/programs/:program_id", automate_rest_api_programs_specific, []}
- , {"/api/v0/users/id/:user_id/programs/id/:program_id/tags", automate_rest_api_program_tags, []}
- , {"/api/v0/users/id/:user_id/programs/id/:program_id/stop-threads", automate_rest_api_program_stop, []}
- , {"/api/v0/users/id/:user_id/programs/id/:program_id/status", automate_rest_api_program_status, []}
-
- , {"/api/v0/users/:user_id/bridges/", automate_rest_api_service_ports_root, []}
- , {"/api/v0/users/id/:user_id/bridges/id/:bridge_id", automate_rest_api_service_ports_specific, []}
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/checkpoint", automate_rest_api_program_specific_checkpoint, []} %% DEPR
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/communication", automate_rest_api_program_specific_logs_stream, []} %% DEPR
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/logs-stream", automate_rest_api_program_specific_logs_stream, []} %% DEPR
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/editor-events", automate_rest_api_program_specific_editor_events, []} %% DEPR
+
+ , {"/api/v0/programs/by-id/:program_id", automate_rest_api_program_specific_by_id, []}
+ , {"/api/v0/programs/by-id/:program_id/checkpoint", automate_rest_api_program_specific_checkpoint, []}
+ , {"/api/v0/programs/by-id/:program_id/logs-stream", automate_rest_api_program_specific_logs_stream, []}
+ , {"/api/v0/programs/by-id/:program_id/variables-stream", automate_rest_api_program_specific_variables_stream, []}
+ , {"/api/v0/programs/by-id/:program_id/editor-events", automate_rest_api_program_specific_editor_events, []}
+ , {"/api/v0/programs/by-id/:program_id/shared-resources", automate_rest_api_program_shared_resources, []}
+ , {"/api/v0/programs/by-id/:program_id/ui-events", automate_rest_api_program_specific_ui_events, []}
+ , {"/api/v0/programs/by-id/:program_id/ui-values", automate_rest_api_program_specific_ui_values, []}
+ , {"/api/v0/programs/by-id/:program_id/custom-blocks", automate_rest_api_program_custom_blocks, []}
+ , {"/api/v0/programs/by-id/:program_id/bridges/by-id/:bridge_id/callbacks/:callback", automate_rest_api_program_bridge_callback, []}
+ , {"/api/v0/programs/by-id/:program_id/render/[...]", automate_rest_api_program_render, []}
+
+ %% Program operation
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/logs", automate_rest_api_program_logs, []} %% DEPR
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/tags", automate_rest_api_program_tags, []} %% DEPR
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/stop-threads", automate_rest_api_program_stop, []} %% DEPR
+ , {"/api/v0/users/id/:user_id/programs/id/:program_id/status", automate_rest_api_program_status, []} %% DEPR
+
+ , {"/api/v0/programs/by-id/:program_id/logs", automate_rest_api_program_logs, []}
+ , {"/api/v0/programs/by-id/:program_id/tags", automate_rest_api_program_tags, []}
+ , {"/api/v0/programs/by-id/:program_id/stop-threads", automate_rest_api_program_stop, []}
+ , {"/api/v0/programs/by-id/:program_id/status", automate_rest_api_program_status, []}
+ , {"/api/v0/programs/by-id/:program_id/assets", automate_rest_api_program_assets_root, []}
+ , {"/api/v0/programs/by-id/:program_id/assets/by-id/:asset_id", automate_rest_api_program_assets_by_id, []}
+ , {"/api/v0/programs/by-id/:program_id/variables", automate_rest_api_program_variables_root, []}
+ , {"/api/v0/programs/by-id/:program_id/variables/:var_name", automate_rest_api_program_variables_specific, []}
+
+ %% Connection management
+ , {"/api/v0/users/id/:user_id/connections/available", automate_rest_api_connections_available_root, []}
+ , {"/api/v0/users/id/:user_id/connections/established", automate_rest_api_connections_established_root, []}
+ , {"/api/v0/users/id/:user_id/connections/pending/:connection_id/wait", automate_rest_api_connections_pending_wait, []}
+
+ , {"/api/v0/groups/by-id/:group_id/connections/established", automate_rest_api_group_connections_established_root, []}
+ , {"/api/v0/groups/by-id/:group_id/connections/available", automate_rest_api_group_connections_available_root, []}
+
+ , {"/api/v0/programs/by-id/:program_id/services/by-id/:service_id/register", automate_rest_api_program_connections_register_root, []}
+ , {"/api/v0/programs/by-id/:program_id/services/by-id/:service_id/how-to-enable", automate_rest_api_services_how_to_enable_new_enable, []}
+ , {"/api/v0/programs/by-id/:program_id/connections/established", automate_rest_api_program_connections_established_root, []}
+ , {"/api/v0/programs/by-id/:program_id/connections/available", automate_rest_api_program_connections_available_root, []}
+
+ %% Bridges
+ , {"/api/v0/users/:user_id/bridges", automate_rest_api_service_ports_root, []}
+ , {"/api/v0/users/id/:user_id/bridges", automate_rest_api_user_bridges_root, []}
+ , {"/api/v0/users/id/:user_id/bridges/id/:bridge_id", automate_rest_api_service_ports_specific, []} %% DEPR
, {"/api/v0/users/id/:user_id/bridges/id/:bridge_id/callback/:callback", automate_rest_api_bridge_callback, []}
, {"/api/v0/users/id/:user_id/bridges/id/:bridge_id/functions/:function", automate_rest_api_bridge_function_specific, []}
, {"/api/v0/users/id/:user_id/bridges/id/:bridge_id/signals", automate_rest_api_bridge_signal_root, []}
+ , {"/api/v0/users/id/:user_id/bridges/id/:bridge_id/signals/:key", automate_rest_api_bridge_signal_specific, []}
, {"/api/v0/users/id/:user_id/bridges/id/:service_port_id/communication"
- , automate_rest_api_service_ports_specific_communication, []}
+ , automate_rest_api_service_ports_specific_communication, []} %% DEPR
, {"/api/v0/users/id/:user_id/bridges/id/:service_port_id/oauth_return"
- , automate_rest_api_service_port_oauth_return, []}
+ , automate_rest_api_service_port_oauth_return, []} %% DPR
+ %% New bridges API
+ , {"/api/v0/bridges/by-id/:bridge_id", automate_rest_api_service_ports_specific, []}
+ , {"/api/v0/bridges/by-id/:service_port_id/communication"
+ , automate_rest_api_service_ports_specific_communication, []}
+ , {"/api/v0/bridges/by-id/:bridge_id/signals", automate_rest_api_bridge_signal_root, []}
+ , {"/api/v0/bridges/by-id/:bridge_id/signals/history", automate_rest_api_bridge_signal_history, []}
+ , {"/api/v0/bridges/by-id/:bridge_id/resources", automate_rest_api_bridge_resources_root, []}
+ , {"/api/v0/connections/by-id/:connection_id", automate_rest_api_connection_by_id, []}
+ , {"/api/v0/connections/by-id/:connection_id/resources/by-name/:resource_name", automate_rest_api_connection_resource_by_name_root, []}
+ , {"/api/v0/bridges/by-id/:bridge_id/tokens", automate_rest_api_bridge_tokens_root, []}
+ , {"/api/v0/bridges/by-id/:bridge_id/tokens/by-name/:token_name", automate_rest_api_bridge_tokens_by_name_root, []}
+ , {"/api/v0/bridges/by-id/:service_port_id/oauth_return"
+ , automate_rest_api_service_port_oauth_return, []} %% DPR
+
+ %% Services
, {"/api/v0/users/:user_id/services", automate_rest_api_services_root, []}
, {"/api/v0/users/:user_id/services/id/:service_id/how-to-enable", automate_rest_api_services_how_to_enable, []}
, {"/api/v0/users/:user_id/services/id/:service_id/register", automate_rest_api_services_register, []}
+
+ , {"/api/v0/services/by-id/:service_id/how-to-enable", automate_rest_api_services_how_to_enable_new, []}
+ , {"/api/v0/services/by-id/:service_id/register", automate_rest_api_services_register_new, []}
+
+ , {"/api/v0/programs/by-id/:program_id/services", automate_rest_api_program_services_root, []}
+
+ %% Groups
+ , {"/api/v0/groups", automate_rest_api_groups_root, [] }
+ , {"/api/v0/groups/by-name/:group_name", automate_rest_api_group_by_name, [] }
+ , {"/api/v0/groups/by-name/:group_name/profile", automate_rest_api_group_profile_by_name, [] }
+ , {"/api/v0/groups/by-id/:group_id", automate_rest_api_group_specific, [] }
+ , {"/api/v0/groups/by-id/:group_id/programs", automate_rest_api_group_programs, [] }
+ , {"/api/v0/groups/by-id/:group_id/collaborators", automate_rest_api_group_collaborators, [] }
+ , {"/api/v0/groups/by-id/:group_id/picture", automate_rest_api_group_picture, [] }
+ , {"/api/v0/groups/by-id/:group_id/bridges", automate_rest_api_group_bridge_root, [] }
+ , {"/api/v0/groups/by-id/:group_id/shared-resources", automate_rest_api_group_shared_resources, [] }
+
+ , {"/api/v0/groups/by-id/:group_id/assets", automate_rest_api_user_assets, [group]}
+ , {"/api/v0/groups/by-id/:group_id/assets/by-id/:asset_id", automate_rest_api_user_asset_by_id, [group]}
+
+ %% Monitor
, {"/api/v0/users/:user_id/monitors", automate_rest_api_monitors_root, []}
+ , {"/api/v0/programs/by-id/:program_id/monitors", automate_rest_api_program_monitors_root, []}
+
+ %% Utils
+ , {"/api/v0/utils/autocomplete/users", automate_rest_api_autocomplete_user, []}
]}
]),
Port = get_port(),
+
+ %% Prepare API metrics
+ prometheus_histogram:declare([ { name, automate_api_latency}
+ , { labels, [ endpoint, user_agent_bucket, error ] }
+ , { buckets, [ 1, 5, 10, 100
+ , 300, 500, 750, 1_000
+ , 2_000, 5_000, 10_000, 30_000
+ , 60_000, 120_000, 300_000
+ ]}
+ , { help, "API latency."}
+ ]),
+ %% Start listening
+ io:fwrite("== Listening on: ~p~n", [Port]),
Start = cowboy:start_clear(http, [{port, Port}],
#{
env => #{dispatch => Dispatch}
@@ -73,6 +197,12 @@ start_link() ->
{ok,
spawn(fun () -> handler(Pid) end)};
+ %% For debug purposes
+ { error, { already_started, Pid } } ->
+ { ok
+ , Pid
+ };
+
%% For debug purposes
{error, eaddrinuse} ->
{ok, spawn(fun() -> handler(none) end)}
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_service_port_oauth_return.erl b/backend/apps/automate_rest_api/src/automate_rest_api_service_port_oauth_return.erl
index 53cb68c1..46f9aee6 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_service_port_oauth_return.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_service_port_oauth_return.erl
@@ -10,11 +10,12 @@
]).
-export([ to_json/2
+ , to_html/2
]).
-include("./records.hrl").
--record(state, { service_port_id }).
+-record(state, { service_port_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -31,13 +32,13 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[SPService] Returning OAuth~n", []),
{[<<"GET">>, <<"OPTIONS">>], Req, State}.
%% GET handler
content_types_provided(Req, State) ->
- io:fwrite("User > service-port > oauth-return~n", []),
- {[{{<<"application">>, <<"json">>, []}, to_json}],
+ {[ {{<<"application">>, <<"json">>, []}, to_json}
+ , {{<<"text">>, <<"html">>, []}, to_html}
+ ],
Req, State}.
-spec to_json(cowboy_req:req(), #state{})
@@ -59,11 +60,40 @@ to_json(Req, State) ->
_ -> 500
end,
- cowboy_req:reply(Code,
- #{ <<"content-type">> => <<"application/json">> },
- jiffy:encode(#{ <<"success">> => false
- , <<"message">> => Reason
- }),
- Req)
+ Res = cowboy_req:reply(Code,
+ #{ <<"content-type">> => <<"application/json">> },
+ jiffy:encode(#{ <<"success">> => false
+ , <<"message">> => Reason
+ }),
+ Req),
+ { stop, Res, State }
end.
+-spec to_html(cowboy_req:req(), #state{})
+ -> {binary() | stop,cowboy_req:req(), #state{}}.
+to_html(Req, State) ->
+ #state{service_port_id=ServicePortId} = State,
+ Qs = cowboy_req:qs(Req),
+ case automate_rest_api_backend:send_oauth_return(ServicePortId, Qs) of
+ ok ->
+ {ok, NewReq} = cowboy_req:reply(
+ 307,
+ #{ <<"Location">> => automate_configuration:get_frontend_root_url()},
+ <<>>,
+ Req),
+ {stop, NewReq, State};
+ {error, Reason} ->
+
+ Code = case Reason of
+ not_found -> 404;
+ unauthorized -> 403;
+ _ -> 500
+ end,
+
+ Res = cowboy_req:reply(Code,
+ #{ <<"content-type">> => <<"text/plain">> },
+ binary:list_to_bin(
+ lists:flatten(io_lib:format("Error performing authentication: '~s'", [Reason]))),
+ Req),
+ {stop, Res, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_root.erl
index 24907eda..26bf9526 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_root.erl
@@ -18,10 +18,12 @@
, to_json/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
-include("../../automate_service_port_engine/src/records.hrl").
+-define(FORMATTING, automate_rest_api_utils_formatting).
--record(state, {username}).
+-record(state, {username :: binary()}).
-spec init(_, _) -> {cowboy_rest, _, _}.
@@ -44,7 +46,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(), _) -> {[binary()], cowboy_req:req(), _}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"POST">>, <<"GET">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -52,7 +53,7 @@ is_authorized(Req, State) ->
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> -> {true, Req1, State};
- _ ->
+ Method ->
case cowboy_req:header(<<"authorization">>, Req,
undefined)
of
@@ -60,8 +61,12 @@ is_authorized(Req, State) ->
{{false, <<"Authorization header not found">>}, Req1,
State};
X ->
+ Scope = case Method of
+ <<"GET">> -> list_bridges;
+ <<"POST">> -> create_bridges
+ end,
#state{username = Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ case automate_rest_api_backend:is_valid_token(X, Scope) of
{true, Username} -> {true, Req1, State};
{true, _} -> %% Non matching username
{{false, <<"Unauthorized to create a program here">>},
@@ -74,7 +79,6 @@ is_authorized(Req, State) ->
%% GET handler
content_types_provided(Req, State) ->
- io:fwrite("User > Bridge > ID~n", []),
{[{{<<"application">>, <<"json">>, []}, to_json}],
Req, State}.
@@ -84,7 +88,7 @@ to_json(Req, State) ->
#state{username=Username} = State,
case automate_rest_api_backend:list_bridges(Username) of
{ ok, Bridges } ->
- Output = jiffy:encode(lists:map(fun to_map/1, Bridges)),
+ Output = jiffy:encode(lists:map(fun ?FORMATTING:bridge_to_json/1, Bridges)),
Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
@@ -92,19 +96,6 @@ to_json(Req, State) ->
{ Output, Res2, State }
end.
-to_map(#service_port_entry_extra{ id=Id
- , name=Name
- , owner=Owner
- , service_id=ServiceId
- , is_connected=IsConnected
- }) ->
- #{ <<"id">> => Id
- , <<"name">> => Name
- , <<"owner">> => Owner
- , <<"service_id">> => ServiceId
- , <<"is_connected">> => IsConnected
- }.
-
%% POST handler
content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, []},
@@ -119,12 +110,14 @@ content_types_accepted(Req, State) ->
accept_json_create_service_port(Req, State) ->
#state{username = Username} = State,
- {ok, Body, Req1} = read_body(Req),
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
#{ <<"name">> := ServicePortName } = jiffy:decode(Body, [return_maps]),
case automate_rest_api_backend:create_service_port(Username, ServicePortName) of
- {ok, ServicePortUrl} ->
- Output = jiffy:encode(#{<<"control_url">> => ServicePortUrl}),
+ {ok, {ServicePortUrl, ServicePortId}} ->
+ Output = jiffy:encode(#{ control_url => ServicePortUrl
+ , id => ServicePortId
+ }),
Res2 = cowboy_req:set_resp_body(Output, Req1),
Res3 = cowboy_req:delete_resp_header(<<"content-type">>,
Res2),
@@ -132,13 +125,3 @@ accept_json_create_service_port(Req, State) ->
<<"application/json">>, Res3),
{{true, ServicePortUrl}, Res4, State}
end.
-
-read_body(Req0) -> read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} ->
- {ok, <>, Req};
- {more, Data, Req} ->
- read_body(Req, <>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific.erl b/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific.erl
index 210ea01e..c7df292f 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific.erl
@@ -13,16 +13,17 @@
-include("./records.hrl").
-include("../../automate_service_port_engine/src/records.hrl").
--record(state, { user_id, bridge_id }).
+-record(state, { bridge_id :: binary()
+ , permissions :: owner_id() | undefined
+ }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
- UserId = cowboy_req:binding(user_id, Req),
BridgeId = cowboy_req:binding(bridge_id, Req),
Req1 = automate_rest_api_cors:set_headers(Req),
{cowboy_rest, Req1
- , #state{ user_id=UserId
- , bridge_id=BridgeId
+ , #state{ bridge_id=BridgeId
+ , permissions=undefined
}}.
%% CORS
@@ -32,27 +33,31 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[SPProgram]Asking for methods~n", []),
{[<<"DELETE">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{bridge_id=BridgeId}) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+ Method ->
+ Check = case Method of
+ <<"DELETE">> -> fun automate_storage:can_user_admin_as/2
+ end,
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
- #state{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, { delete_bridge, BridgeId }) of
{true, UserId} ->
- { true, Req1, State };
- {true, TokenUserId} -> %% Non matching user_id
- io:fwrite("Url UID: ~p | Token UID: ~p~n", [UserId, TokenUserId]),
- { { false, <<"Unauthorized to create a program here">>}, Req1, State };
+ {ok, Owner} = automate_service_port_engine:get_bridge_owner(BridgeId),
+ case Check({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ permissions=Owner } };
+ _ ->
+ automate_logging:log_api(warning, ?MODULE, io_lib:format("Resource owner: ~p | Token UID: ~p~n", [Owner, UserId])),
+ { { false, <<"Unauthorized operation">>}, Req1, State }
+ end;
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
end
@@ -61,14 +66,13 @@ is_authorized(Req, State) ->
%% DELETE handler
-delete_resource(Req, State) ->
- #state{bridge_id=BridgeId, user_id=UserId} = State,
- case automate_rest_api_backend:delete_bridge(UserId, BridgeId) of
+delete_resource(Req, State=#state{bridge_id=BridgeId, permissions=Owner}) ->
+ case automate_service_port_engine:delete_bridge(Owner, BridgeId) of
ok ->
Req1 = send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
{ true, Req1, State };
{ error, Reason } ->
- Req1 = send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req),
+ Req1 = send_json_output(jiffy:encode(#{ <<"success">> => false, <<"debug">> => list_to_binary(io_lib:format("~p", [Reason])) }), Req),
{ false, Req1, State }
end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific_communication.erl b/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific_communication.erl
index 1c89edeb..f620e7fe 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific_communication.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_service_ports_specific_communication.erl
@@ -7,80 +7,89 @@
-export([websocket_init/1]).
-export([websocket_handle/2]).
-export([websocket_info/2]).
+-export([terminate/3]).
--record(state, { user_id :: binary()
+-include("../../automate_common_types/src/types.hrl").
+-include("../../automate_common_types/src/protocol.hrl").
+
+-record(state, { owner :: owner_id()
, service_port_id :: binary()
- , user_channels :: #{ binary() := any() }
+ , user_channels :: #{ owner_id() := any() }
+ , authenticated :: boolean()
}).
init(Req, _Opts) ->
- UserId = cowboy_req:binding(user_id, Req),
ServicePortId = cowboy_req:binding(service_port_id, Req),
-
+ {ok, Owner} = automate_service_port_engine:get_bridge_owner(ServicePortId),
{cowboy_websocket, Req, #state{ service_port_id=ServicePortId
- , user_id=UserId
+ , owner=Owner
, user_channels=#{}
+ , authenticated=false
}}.
-websocket_init(State=#state{ service_port_id=ServicePortId
- }) ->
- automate_service_port_engine:register_service_port(ServicePortId),
+websocket_init(State=#state{}) ->
{ok, State}.
-websocket_handle({text, Msg}, State=#state{ service_port_id=ServicePortId
- , user_id=UserId
- }) ->
- automate_service_port_engine:from_service_port(ServicePortId, UserId, Msg),
- {ok, State};
+websocket_handle({text, Msg}, State) ->
+ handle_bridge_message(Msg, State);
-websocket_handle({binary, Msg}, State=#state{ service_port_id=ServicePortId
- , user_id=UserId
- }) ->
- automate_service_port_engine:from_service_port(ServicePortId, UserId, Msg),
- {ok, State};
+websocket_handle({binary, Msg}, State) ->
+ handle_bridge_message(Msg, State);
websocket_handle(_Message, State) ->
{ok, State}.
websocket_info({automate_service_port_engine_router, _From, { data, MessageId, Message }}, State) ->
- io:fwrite("[~p] New message: ~p~n", [MessageId, Message]),
Serialized = jiffy:encode(Message#{ <<"message_id">> => MessageId }),
{reply, {binary, Serialized}, State};
websocket_info({{ automate_service_port_engine, advice_taken}, MessageId, AdviceTaken}, State) ->
- io:fwrite("[~p] Advice taken: ~p~n", [MessageId, AdviceTaken]),
+ automate_logging:log_api(debug, ?MODULE, {advice_taken, MessageId, AdviceTaken}),
Serialized = jiffy:encode(#{ <<"type">> => <<"ADVICE_RESPONSE">>
, <<"message_id">> => MessageId
, <<"value">> => AdviceTaken
}),
{reply, {binary, Serialized}, State};
+websocket_info({{ automate_service_port_engine, request_icon}}, State=#state{ service_port_id=ServicePortId }) ->
+ automate_logging:log_api(debug, ?MODULE, {requesting_icon, ServicePortId}),
+ Serialized = jiffy:encode(#{ <<"type">> => <<"ICON_REQUEST">>
+ }),
+ {reply, {binary, Serialized}, State};
+
websocket_info({ automate_service_port_engine, new_channel, {_ServicePortId, ChannelId}}, State) ->
ok = automate_channel_engine:monitor_listeners(ChannelId, self(), node()),
{ok, State};
websocket_info({ automate_channel_engine, add_listener, {Pid, Key, SubKey}}, State=#state{service_port_id=ServicePortId}) ->
case automate_bot_engine:get_user_from_pid(Pid) of
- {ok, UserId} ->
- {ok, ServicePortUserId} = automate_service_port_engine:internal_user_id_to_service_port_user_id(UserId, ServicePortId),
- {UserChannels, NewState} = add_to_user_channels(UserId, {Key, SubKey}, State),
- Serialized = jiffy:encode(#{ <<"type">> => <<"ADVICE_NOTIFICATION">>
- , <<"value">> =>
- #{ <<"SIGNAL_LISTENERS">> =>
- #{
- ServicePortUserId => fmt_user_data(UserChannels)
- }
- }
- }),
- {reply, {binary, Serialized}, NewState};
+ {ok, Owner} ->
+ %% TODO: In this instance is probably OK to use a single connection
+ %% as the focus are the values, not the keys of SIGNAL_LISTENERS.
+ %% But it can be disambiguated by passing more "properties" on the 'add_listener' message.
+ case automate_service_port_engine:internal_user_id_to_connection_id(Owner, ServicePortId) of
+ {ok, ConnectionId} ->
+ {UserChannels, NewState} = add_to_user_channels(Owner, {Key, SubKey}, State),
+ Serialized = jiffy:encode(#{ <<"type">> => <<"ADVICE_NOTIFICATION">>
+ , <<"value">> =>
+ #{ <<"SIGNAL_LISTENERS">> =>
+ #{
+ ConnectionId => fmt_user_data(UserChannels)
+ }
+ }
+ }),
+ {reply, {binary, Serialized}, NewState};
+ {error, Reason} ->
+ automate_logging:log_api(error, ?MODULE, {error, Reason}),
+ {ok, State}
+ end;
{error, not_found} ->
{ok, State}
end;
websocket_info(Message, State) ->
- io:fwrite("Got ~p~n", [Message]),
- {reply, {binary, Message}, State}.
-
+ automate_logging:log_api(warning, ?MODULE, {unexpected_message, Message}),
+ {ok, State}.
%% State maintenance
merge_user_data(UserData, ChannelData) ->
@@ -101,13 +110,118 @@ fmt_channel_data({ Key, SubKey }) ->
, <<"subkey">> => SubKey
}.
-add_to_user_channels(UserId, ChannelData, State=#state{user_channels=UserChannels}) ->
+add_to_user_channels(Owner, ChannelData, State=#state{user_channels=UserChannels}) ->
case UserChannels of
- #{ UserId := UserData } ->
+ #{ Owner := UserData } ->
NewUserData = merge_user_data(UserData, ChannelData),
- { NewUserData, State#state{ user_channels=UserChannels#{ UserId => NewUserData } } };
+ { NewUserData, State#state{ user_channels=UserChannels#{ Owner => NewUserData } } };
_ ->
NewUserData = create_user_data(ChannelData),
- { NewUserData, State#state{ user_channels=UserChannels#{ UserId => NewUserData } } }
+ { NewUserData, State#state{ user_channels=UserChannels#{ Owner => NewUserData } } }
end.
+
+handle_bridge_message(Msg, State=#state{ service_port_id=BridgeId
+ , authenticated=false
+ , owner=Owner
+ }) ->
+ Data = jiffy:decode(Msg, [return_maps]),
+ Passed = case Data of
+ #{ <<"type">> := <<"AUTHENTICATION">>
+ , <<"value">> := #{ <<"token">> := Token
+ }
+ } ->
+ case automate_service_port_engine:check_bridge_token(BridgeId, Token) of
+ {ok, true} -> true;
+ {ok, false} ->
+ {false, mismatch}
+ end;
+ _ ->
+ {ok, Answer} = automate_service_port_engine:can_skip_authentication(BridgeId),
+ case Answer of
+ true -> skip;
+ false -> {false, not_found}
+ end
+ end,
+ case Passed of
+ true ->
+ WasConnectionBefore = automate_service_port_engine:is_bridge_connected(BridgeId),
+ ok = automate_service_port_engine:register_service_port(BridgeId),
+ on_new_connection(BridgeId, Owner, WasConnectionBefore),
+
+ {ok, State#state{ authenticated=true }};
+ skip ->
+ WasConnectionBefore = automate_service_port_engine:is_bridge_connected(BridgeId),
+ ok = automate_service_port_engine:register_service_port(BridgeId),
+ on_new_connection(BridgeId, Owner, WasConnectionBefore),
+
+ handle_bridge_message(Msg, State#state{ authenticated=true });
+ {false, Reason} ->
+ automate_logging:log_api(warning, ?MODULE,
+ binary:list_to_bin(lists:flatten(io_lib:format("Authentication error on bridge_id=~p (~p)",
+ [ BridgeId, Reason ])))),
+ { reply
+ , { close
+ , case Reason of
+ mismatch -> <<"Not matching token">>;
+ not_found -> <<"Token not found">>
+ end
+ }
+ , State
+ }
+ end;
+
+handle_bridge_message(Msg, State=#state{ service_port_id=ServicePortId
+ , owner=Owner
+ }) ->
+ try automate_service_port_engine:from_service_port(ServicePortId, Owner, jiffy:decode(Msg, [return_maps])) of
+ _ ->
+ {ok, State}
+ catch ErrorNs:Error:StackTrace ->
+ automate_logging:log_api(error, ?MODULE, binary:list_to_bin(
+ lists:flatten(io_lib:format("~p:~p~n~p", [ErrorNs, Error, StackTrace])))),
+ { reply
+ , { close
+ , binary:list_to_bin(
+ lists:flatten(io_lib:format("~p~p", [ErrorNs, Error])))}
+ , State
+ }
+ end.
+
+terminate(Reason, _PartialReq, #state{ service_port_id=BridgeId
+ , owner=Owner
+ }) ->
+ automate_logging:log_api(warning, ?MODULE, list_to_binary(io_lib:format("Bridge (id=~0tp) disconnected with reason: '~0tp'", [BridgeId, Reason]))),
+ ok = automate_service_port_engine:unregister_service_port(BridgeId),
+ case automate_service_port_engine:is_bridge_connected(BridgeId) of
+ {ok, true} ->
+ %% There's still a remaining connection
+ ok;
+ _ ->
+ %% No remaining connections
+ Msg = #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => ?PROTO_ON_BRIDGE_DISCONNECTED
+ , <<"subkey">> => BridgeId
+ , <<"to_user">> => null
+ , <<"value">> => <<"disconnected">>
+ , <<"content">> => <<"disconnected">>
+ },
+ ok = automate_service_port_engine:from_service_port(BridgeId, Owner, Msg)
+ end.
+
+on_new_connection(BridgeId, Owner, WasConnectionBefore) ->
+ case WasConnectionBefore of
+ {ok, true} ->
+ %% There was a connection before this one
+ ok;
+ _ ->
+ %% This connection makes the bridge usable
+ Msg = #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => ?PROTO_ON_BRIDGE_CONNECTED
+ , <<"subkey">> => BridgeId
+ , <<"to_user">> => null
+ , <<"value">> => <<"connected">>
+ , <<"content">> => <<"connected">>
+ },
+ ok = automate_service_port_engine:from_service_port(BridgeId, Owner, Msg)
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_services_how_to_enable.erl b/backend/apps/automate_rest_api/src/automate_rest_api_services_how_to_enable.erl
index 7a16b677..ea1ea8f2 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_services_how_to_enable.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_services_how_to_enable.erl
@@ -15,7 +15,7 @@
-include("./records.hrl").
--record(state, { username, service_id }).
+-record(state, { username :: binary(), service_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -34,10 +34,9 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[SPService]Asking for methods~n", []),
- {[<<"GET">>, <<"PUT">>, <<"OPTIONS">>], Req, State}.
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{ service_id=ServiceId }) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
@@ -49,7 +48,7 @@ is_authorized(Req, State) ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
#state{username=Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ case automate_rest_api_backend:is_valid_token(X, { read_how_to_enable_service, ServiceId }) of
{true, Username} ->
{ true, Req1, State };
{true, _} -> %% Non matching username
@@ -62,7 +61,6 @@ is_authorized(Req, State) ->
%% Get handler
content_types_provided(Req, State) ->
- io:fwrite("User > service > ID~n", []),
{[{{<<"application">>, <<"json">>, []}, to_json}],
Req, State}.
@@ -76,18 +74,46 @@ to_json(Req, State) ->
Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
{ jiffy:encode(extend_how_to(HowTo, ServiceId)), Res2, State };
- {error, not_found} ->
- Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
- Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
- %% TODO: Return 404
- { jiffy:encode(#{ <<"success">> => false, <<"message">> => <<"Service not found">> }),
- Res2, State }
+ {error, Reason} ->
+ automate_logging:log_api(error, ?MODULE, binary:list_to_bin(lists:flatten(io_lib:format("~p", [Reason])))),
+ Code = case Reason of
+ not_found -> 404;
+ no_connection -> 409;
+ _ -> 500
+ end,
+ Output = jiffy:encode(#{ <<"success">> => false
+ , <<"message">> => case Reason of
+ X when is_atom(X) -> X;
+ _ -> error
+ end
+ }),
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
end.
+extend_how_to(HowTo=#{ <<"type">> := <<"form">>
+ , <<"connection_id">> := ConnectionId }, ServiceId) ->
+ Restructured = HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId
+ , <<"connection_id">> => ConnectionId
+ } },
+ maps:remove(<<"connection_id">>, Restructured);
+
+extend_how_to(HowTo=#{ <<"type">> := <<"message">>
+ , <<"connection_id">> := ConnectionId }, ServiceId) ->
+ Restructured = HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId
+ , <<"connection_id">> => ConnectionId
+ } },
+ maps:remove(<<"connection_id">>, Restructured);
+
extend_how_to(HowTo=#{ <<"type">> := <<"form">> }, ServiceId) ->
HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId } };
+extend_how_to(HowTo=#{ <<"type">> := <<"message">> }, ServiceId) ->
+ HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId } };
+
+extend_how_to(HowTo=#{ <<"type">> := <<"direct">> }, ServiceId) ->
+ HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId } };
+
extend_how_to(HowTo, _ServiceId) ->
HowTo.
-
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_services_how_to_enable_new.erl b/backend/apps/automate_rest_api/src/automate_rest_api_services_how_to_enable_new.erl
new file mode 100644
index 00000000..950c1ed9
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_services_how_to_enable_new.erl
@@ -0,0 +1,149 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_services_how_to_enable_new).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { group_id :: binary() | undefined
+ , program_id :: binary() | undefined
+ , owner :: owner_id() | undefined
+ , service_id :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ServiceId = cowboy_req:binding(service_id, Req),
+ Qs = cowboy_req:parse_qs(Req),
+ GroupId = proplists:get_value(<<"group_id">>, Qs),
+ ProgramId = proplists:get_value(<<"program_id">>, Qs),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ group_id=GroupId
+ , service_id=ServiceId
+ , program_id=ProgramId
+ , owner=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId, group_id=GroupId, service_id=BridgeId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ Method ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {read_how_to_enable_service, BridgeId}) of
+ {true, UserId} ->
+ case {ProgramId, GroupId} of
+ {Pid, _} when is_binary(Pid) ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:can_user_edit_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ {undefined, G} when is_binary(G) ->
+ case automate_storage:is_allowed_to_write_in_group({user, UserId}, GroupId) of
+ true ->
+ { true, Req1, State#state{owner={group, GroupId}} };
+ false ->
+ { { false, <<"Unauthorized to create a service here">>}, Req1, State }
+ end;
+ {undefined, undefined} ->
+ { true, Req1, State#state{owner={user, UserId}} }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% Get handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{owner=Owner, service_id=ServiceId}) ->
+ case get_how_to(Owner, ServiceId) of
+ { ok, HowTo } ->
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { jiffy:encode(extend_how_to(HowTo, ServiceId)), Res2, State };
+ {error, Reason} ->
+ automate_logging:log_api(error, ?MODULE, binary:list_to_bin(lists:flatten(io_lib:format("~p", [Reason])))),
+ Code = case Reason of
+ not_found -> 404;
+ no_connection -> 409;
+ _ -> 500
+ end,
+ Output = jiffy:encode(#{ <<"success">> => false
+ , <<"message">> => case Reason of
+ X when is_atom(X) -> X;
+ _ -> error
+ end
+ }),
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
+ end.
+
+get_how_to(Owner, ServiceId) ->
+ case automate_service_registry:get_service_by_id(ServiceId) of
+ E = {error, _} ->
+ E;
+ {ok, #{ module := Module }} ->
+ automate_service_registry_query:get_how_to_enable(Module, Owner)
+ end.
+
+extend_how_to(HowTo=#{ <<"type">> := <<"form">>
+ , <<"connection_id">> := ConnectionId
+ }, ServiceId) ->
+ Restructured = HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId
+ , <<"connection_id">> => ConnectionId
+ } },
+ maps:remove(<<"connection_id">>, Restructured);
+
+extend_how_to(HowTo=#{ <<"type">> := <<"message">>
+ , <<"connection_id">> := ConnectionId }, ServiceId) ->
+ Restructured = HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId
+ , <<"connection_id">> => ConnectionId
+ } },
+ maps:remove(<<"connection_id">>, Restructured);
+
+extend_how_to(HowTo=#{ <<"type">> := <<"form">> }, ServiceId) ->
+ HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId } };
+
+extend_how_to(HowTo=#{ <<"type">> := <<"message">> }, ServiceId) ->
+ HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId } };
+
+extend_how_to(HowTo=#{ <<"type">> := <<"direct">> }, ServiceId) ->
+ HowTo#{ <<"metadata">> => #{ <<"service_id">> => ServiceId } };
+
+extend_how_to(HowTo, _ServiceId) ->
+ HowTo.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_services_register.erl b/backend/apps/automate_rest_api/src/automate_rest_api_services_register.erl
index 02ad0ebb..d3b5e040 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_services_register.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_services_register.erl
@@ -13,9 +13,10 @@
-export([ accept_json_register_service/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(state, { username, service_id }).
+-record(state, { username :: binary(), service_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -34,7 +35,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[Service] Asking for methods~n", []),
{[<<"POST">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -49,7 +49,7 @@ is_authorized(Req, State) ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
#state{username=Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ case automate_rest_api_backend:is_valid_token(X, create_services) of
{true, Username} ->
{ true, Req1, State };
{true, _} -> %% Non matching username
@@ -71,26 +71,19 @@ content_types_accepted(Req, State) ->
#state{}) -> {true, cowboy_req:req(), #state{}}.
accept_json_register_service(Req, State) ->
#state{username = Username, service_id = ServiceId} = State,
- {ok, Body, Req1} = read_body(Req),
- RegistrationData = jiffy:decode(Body, [return_maps]),
-
- case automate_rest_api_backend:register_service(Username, ServiceId, RegistrationData) of
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ FullRegistrationData = jiffy:decode(Body, [return_maps]),
+ { RegistrationData, ConnectionId } = case FullRegistrationData of
+ #{ <<"metadata">> := #{<<"connection_id">> := ConnId} } ->
+ {maps:remove(<<"metadata">>, FullRegistrationData), ConnId};
+ #{ <<"metadata">> := #{} } ->
+ {maps:remove(<<"metadata">>, FullRegistrationData), undefined};
+ _ ->
+ {FullRegistrationData, undefined}
+ end,
+ case automate_rest_api_backend:register_service(Username, ServiceId, RegistrationData, ConnectionId) of
{ok, Data} ->
Output = jiffy:encode(Data),
- Res2 = cowboy_req:set_resp_body(Output, Req1),
- Res3 = cowboy_req:delete_resp_header(<<"content-type">>,
- Res2),
- Res4 = cowboy_req:set_resp_header(<<"content-type">>,
- <<"application/json">>, Res3),
- {true, Res4, State}
- end.
-
-read_body(Req0) -> read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} ->
- {ok, <>, Req};
- {more, Data, Req} ->
- read_body(Req, <>)
+ Res2 = ?UTILS:send_json_output(Output, Req1),
+ {true, Res2, State}
end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_services_register_new.erl b/backend/apps/automate_rest_api/src/automate_rest_api_services_register_new.erl
new file mode 100644
index 00000000..3f11f91a
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_services_register_new.erl
@@ -0,0 +1,116 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_services_register_new).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ ]).
+
+-export([ accept_json_register_service/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { group_id :: binary() | undefined
+ , program_id :: binary() | undefined
+ , service_id :: binary()
+ , owner :: owner_id() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ServiceId = cowboy_req:binding(service_id, Req),
+ Qs = cowboy_req:parse_qs(Req),
+ GroupId = proplists:get_value(<<"group_id">>, Qs),
+ ProgramId = proplists:get_value(<<"program_id">>, Qs),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ group_id=GroupId
+ , service_id=ServiceId
+ , program_id=ProgramId
+ , owner=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId, group_id=GroupId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, create_services) of
+ {true, UserId} ->
+ case {ProgramId, GroupId} of
+ {Pid, _} when is_binary(Pid) ->
+ {ok, #user_program_entry{ owner=Owner }} = automate_storage:get_program_from_id(ProgramId),
+ case automate_storage:can_user_edit_as({user, UserId}, Owner) of
+ true -> { true, Req1, State#state{ owner=Owner } };
+ false ->
+ { { false, <<"Operation not allowed">>}, Req1, State }
+ end;
+ {_, G} when is_binary(G) ->
+ case automate_storage:is_allowed_to_write_in_group({user, UserId}, GroupId) of
+ true ->
+ { true, Req1, State#state{owner={group, GroupId}} };
+ false ->
+ { { false, <<"Unauthorized to create a service here">>}, Req1, State }
+ end;
+ _ ->
+ { true, Req1, State#state{owner={user, UserId}} }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []},
+ accept_json_register_service}],
+ Req, State}.
+
+-spec accept_json_register_service(cowboy_req:req(),
+ #state{}) -> {true, cowboy_req:req(), #state{}}.
+accept_json_register_service(Req, State=#state{owner=Owner, service_id=ServiceId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ FullRegistrationData = jiffy:decode(Body, [return_maps]),
+ { RegistrationData, ConnectionId } = case FullRegistrationData of
+ #{ <<"metadata">> := #{<<"connection_id">> := ConnId} } ->
+ {maps:remove(<<"metadata">>, FullRegistrationData), ConnId};
+ #{ <<"metadata">> := #{} } ->
+ {maps:remove(<<"metadata">>, FullRegistrationData), undefined};
+ _ ->
+ {FullRegistrationData, undefined}
+ end,
+ case send_registration_data(Owner, ServiceId, RegistrationData, ConnectionId) of
+ {ok, Data} ->
+ Output = jiffy:encode(Data),
+ Res2 = ?UTILS:send_json_output(Output, Req1),
+ {true, Res2, State}
+ end.
+
+send_registration_data(Owner, ServiceId, RegistrationData, ConnectionId) ->
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ {ok, _Result} = automate_service_registry_query:send_registration_data(Module, Owner, RegistrationData,
+ #{<<"connection_id">> => ConnectionId}).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_services_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_services_root.erl
index c23d1ba5..672bafbf 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_services_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_services_root.erl
@@ -16,7 +16,7 @@
-include("./records.hrl").
--record(state, { username }).
+-record(state, { username :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -40,7 +40,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"GET">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -55,7 +54,7 @@ is_authorized(Req, State) ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
#state{username=Username} = State,
- case automate_rest_api_backend:is_valid_token(X) of
+ case automate_rest_api_backend:is_valid_token(X, list_services) of
{true, Username} ->
{ true, Req1, State };
{true, _} -> %% Non matching username
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_check.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_check.erl
index 0ffc0265..3b5bc67b 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_check.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_check.erl
@@ -1,5 +1,5 @@
%%% @doc
-%%% REST endpoint to manage knowledge collections.
+%%% REST endpoint to inspect user preferences.
%%% @end
-module(automate_rest_api_sessions_check).
@@ -12,13 +12,14 @@
-export([to_json/2]).
-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
--record(check_seq, { username }).
+-record(check_seq, { user_id :: binary() | undefined }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
{cowboy_rest, Req
- , #check_seq{ username=undefined }}.
+ , #check_seq{ user_id=undefined }}.
content_types_provided(Req, State) ->
{[ {<<"application/json">>, to_json}
@@ -31,7 +32,6 @@ options(Req, State) ->
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"GET">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -45,9 +45,9 @@ is_authorized(Req, State) ->
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
- case automate_rest_api_backend:is_valid_token(X) of
- {true, Username} ->
- { true, Req1, #check_seq{username=Username} };
+ case automate_rest_api_backend:is_valid_token_uid(X, check) of
+ {true, UserId} ->
+ { true, Req1, #check_seq{user_id=UserId} };
false ->
{ { false, <<"Authorization not correct">>}, Req1, State }
end
@@ -56,15 +56,32 @@ is_authorized(Req, State) ->
%% GET handler
-spec to_json(cowboy_req:req(), #check_seq{}) -> {binary(),cowboy_req:req(),_}.
-to_json(Req, State) ->
- #check_seq{username=Username} = State,
- {ok, UserId} = automate_storage:get_userid_from_username(Username),
+to_json(Req, State=#check_seq{user_id=UserId}) ->
+ {ok, User} = automate_rest_api_backend:get_user(UserId),
- Output = jiffy:encode(#{ <<"success">> => true
- , <<"username">> => Username
- , <<"user_id">> => UserId
- }),
+ Output = encode_user(User),
Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
{ Output, Res2, State }.
+
+
+encode_user(#registered_user_entry{ id=UserId
+ , canonical_username=Username
+ %% , password
+ %% , email
+ %% , status
+ %% , registration_time
+
+ , is_admin=IsAdmin
+ , is_advanced=IsAdvanced
+ , is_in_preview=IsInPreview
+ }) ->
+ jiffy:encode(#{ success => true
+ , username => Username
+ , user_id => UserId
+ , tags => #{ is_admin => IsAdmin
+ , is_advanced => IsAdvanced
+ , is_in_preview => IsInPreview
+ }
+ }).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_login.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_login.erl
index 1ec4cff9..f8823e87 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_login.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_login.erl
@@ -10,9 +10,11 @@
]).
-export([accept_json_modify_collection/2]).
+
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(login_seq, { rest_session,
+-record(login_seq, { rest_session :: binary() | undefined,
login_data
}).
@@ -34,21 +36,19 @@ options(Req, State) ->
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
Res = automate_rest_api_cors:set_headers(Req),
- io:fwrite("[Login] Asking for methods~n", []),
- {[<<"POST">>, <<"GET">>, <<"OPTIONS">>], Res, State}.
+ {[<<"POST">>, <<"OPTIONS">>], Res, State}.
content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, []}, accept_json_modify_collection}],
Req, State}.
%%%% POST
- %
-spec accept_json_modify_collection(cowboy_req:req(),#login_seq{})
-> {'true',cowboy_req:req(),_}.
accept_json_modify_collection(Req, Session) ->
case cowboy_req:has_body(Req) of
true ->
- {ok, Body, Req2} = read_body(Req),
+ {ok, Body, Req2} = ?UTILS:read_body(Req),
Parsed = [jiffy:decode(Body, [return_maps])],
case to_register_data(Parsed) of
{ ok, LoginData } ->
@@ -68,7 +68,7 @@ accept_json_modify_collection(Req, Session) ->
Res1 = cowboy_req:set_resp_body(jiffy:encode(#{ success => false
, error => reason_to_json(Reason)
}), Req2),
- io:format("Error logging in: ~p~n", [Reason]),
+ automate_logging:log_api(error, ?MODULE, Reason),
{ false, Res1, Session}
end;
{ error, _Reason } ->
@@ -96,12 +96,3 @@ to_register_data([#{ <<"password">> := Password
to_register_data(_X) ->
{ error, "Data structures not matching" }.
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register.erl
index ef301702..ecba53aa 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register.erl
@@ -11,19 +11,18 @@
-export([resource_exists/2]).
-export([accept_json_modify_collection/2]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMAT, automate_rest_api_utils_formatting).
-include("./records.hrl").
--record(registration_seq, { rest_session,
- registration_data
- }).
+-record(state, {}).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
- io:format("Added CORS: ok~n", []),
Res = automate_rest_api_cors:set_headers(Req),
{cowboy_rest, Res
- , #registration_seq{ rest_session=undefined
- , registration_data=undefined}}.
+ , #state{}}.
resource_exists(Req, State) ->
{false, Req, State}.
@@ -38,7 +37,6 @@ options(Req, State) ->
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"POST">>, <<"GET">>, <<"OPTIONS">>], Req, State}.
content_types_accepted(Req, State) ->
@@ -47,12 +45,12 @@ content_types_accepted(Req, State) ->
%%%% POST
%
--spec accept_json_modify_collection(cowboy_req:req(),#registration_seq{})
- -> {'false' | {'true', binary()},cowboy_req:req(),#registration_seq{}}.
+-spec accept_json_modify_collection(cowboy_req:req(),#state{})
+ -> {'false' | {'true', binary()},cowboy_req:req(),#state{}}.
accept_json_modify_collection(Req, Session) ->
case cowboy_req:has_body(Req) of
true ->
- {ok, Body, Req2} = read_body(Req),
+ {ok, Body, Req2} = ?UTILS:read_body(Req),
Parsed = [jiffy:decode(Body, [return_maps])],
case to_register_data(Parsed) of
{ ok, RegistrationData } ->
@@ -68,11 +66,12 @@ accept_json_modify_collection(Req, Session) ->
Res1 = cowboy_req:set_resp_body(Output, Req2),
Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
- { true, Res3, Session#registration_seq{
- registration_data=RegistrationData} };
+ { true, Res3, Session};
{error, Reason} ->
- io:format("Error registering: ~p~n", [Reason]),
- { false, Req2, Session}
+ Res1 = ?UTILS:send_json_output(jiffy:encode(#{ success => false
+ , error => ?FORMAT:reason_to_json(Reason)
+ }), Req2),
+ {false, Res1, Session}
end;
{ error, _Reason } ->
{ false, Req2, Session }
@@ -90,15 +89,5 @@ to_register_data([#{ <<"email">> := Email
, email=Email
} };
-to_register_data(X) ->
- io:format("Found on register: ~p~n", [X]),
+to_register_data(_) ->
{ error, "Data structures not matching" }.
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register_verify.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register_verify.erl
index 9f61af3b..57b821a7 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register_verify.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_register_verify.erl
@@ -10,18 +10,18 @@
]).
-export([accept_json_modify_collection/2]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMAT, automate_rest_api_utils_formatting).
-include("../../automate_storage/src/records.hrl").
-include("./records.hrl").
--record(login_seq, { rest_session,
- login_data
- }).
+-record(state, {}).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
{cowboy_rest, Req
- , #login_seq{ rest_session=undefined
- , login_data=undefined}}.
+ , #state{}}.
%% CORS
options(Req, State) ->
@@ -31,7 +31,6 @@ options(Req, State) ->
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
Res = automate_rest_api_cors:set_headers(Req),
- io:fwrite("[Validate Register] Asking for methods~n", []),
{[<<"POST">>, <<"OPTIONS">>], Res, State}.
content_types_accepted(Req, State) ->
@@ -39,12 +38,12 @@ content_types_accepted(Req, State) ->
Req, State}.
%%%% POST
--spec accept_json_modify_collection(cowboy_req:req(),#login_seq{})
+-spec accept_json_modify_collection(cowboy_req:req(),#state{})
-> {'true',cowboy_req:req(),_}.
accept_json_modify_collection(Req, Session) ->
case cowboy_req:has_body(Req) of
true ->
- {ok, Body, Req2} = read_body(Req),
+ {ok, Body, Req2} = ?UTILS:read_body(Req),
Parsed = jiffy:decode(Body, [return_maps]),
case Parsed of
#{ <<"verification_code">> := VerificationCode } ->
@@ -54,7 +53,7 @@ accept_json_modify_collection(Req, Session) ->
{ok, #registered_user_entry{ username=Username
, status=ready
}} ->
- { ok, Token } = automate_rest_api_backend:generate_token_for_user(UserId),
+ { ok, Token } = automate_storage:generate_token_for_user(UserId, all, session),
Output = jiffy:encode(#{ <<"success">> => true
, <<"session">> => #{ <<"token">> => Token
, <<"user_id">> => UserId
@@ -70,15 +69,15 @@ accept_json_modify_collection(Req, Session) ->
Res1 = cowboy_req:set_resp_body(jiffy:encode(#{ success => false
, error => #{ type => user_not_ready }
}), Req2),
- io:format("Error autologin on verify: user not found or not ready~n"),
+ automate_logging:log_api(error, ?MODULE, "Error autologin on verify: user not found or not ready"),
{ false, Res1, Session}
end;
{error, Reason} ->
Res1 = cowboy_req:set_resp_body(jiffy:encode(#{ success => false
- , error => reason_to_json(Reason)
+ , error => ?FORMAT:reason_to_json(Reason)
}), Req2),
- io:format("Error logging in: ~p~n", [Reason]),
+ automate_logging:log_api(error, ?MODULE, {error, Reason}),
{ false, Res1, Session}
end;
_ ->
@@ -87,20 +86,3 @@ accept_json_modify_collection(Req, Session) ->
false ->
{false, Req, Session }
end.
-
-reason_to_json({Type, Subtype}) ->
- #{ type => Type
- , subtype => Subtype
- };
-reason_to_json(Type) ->
- #{ type => Type
- }.
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password.erl
index 7aa8625e..ccd90e10 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password.erl
@@ -10,14 +10,16 @@
]).
-export([accept_json_modify_collection/2]).
+
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(rest_seq, { rest_session }).
+-record(state, {}).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
{cowboy_rest, Req
- , #rest_seq{ rest_session=undefined }}.
+ , #state{ }}.
%% CORS
options(Req, State) ->
@@ -27,7 +29,6 @@ options(Req, State) ->
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
Res = automate_rest_api_cors:set_headers(Req),
- io:fwrite("[Login] Asking for methods~n", []),
{[<<"POST">>, <<"GET">>, <<"OPTIONS">>], Res, State}.
content_types_accepted(Req, State) ->
@@ -35,12 +36,12 @@ content_types_accepted(Req, State) ->
Req, State}.
%%%% POST
--spec accept_json_modify_collection(cowboy_req:req(),#rest_seq{})
+-spec accept_json_modify_collection(cowboy_req:req(),#state{})
-> {'true',cowboy_req:req(),_}.
accept_json_modify_collection(Req, Session) ->
case cowboy_req:has_body(Req) of
true ->
- {ok, Body, Req2} = read_body(Req),
+ {ok, Body, Req2} = ?UTILS:read_body(Req),
Parsed = jiffy:decode(Body, [return_maps]),
case Parsed of
#{ <<"email">> := Email} ->
@@ -56,7 +57,7 @@ accept_json_modify_collection(Req, Session) ->
Res1 = cowboy_req:set_resp_body(jiffy:encode(#{ success => false
, error => reason_to_json(Reason)
}), Req2),
- io:format("Error logging in: ~p~n", [Reason]),
+ automate_logging:log_api(info, ?MODULE, {error, Reason}),
{ false, Res1, Session}
end;
_ ->
@@ -73,13 +74,3 @@ reason_to_json({Type, Subtype}) ->
reason_to_json(Type) ->
#{ type => Type
}.
-
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_update.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_update.erl
index 3a2f354a..f032789a 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_update.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_update.erl
@@ -10,17 +10,16 @@
]).
-export([accept_json_modify_collection/2]).
+
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(login_seq, { rest_session,
- login_data
- }).
+-record(state, { }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
{cowboy_rest, Req
- , #login_seq{ rest_session=undefined
- , login_data=undefined}}.
+ , #state{}}.
%% CORS
options(Req, State) ->
@@ -30,7 +29,6 @@ options(Req, State) ->
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
Res = automate_rest_api_cors:set_headers(Req),
- io:fwrite("[Password reset/Update] Asking for methods~n", []),
{[<<"POST">>, <<"OPTIONS">>], Res, State}.
content_types_accepted(Req, State) ->
@@ -38,12 +36,12 @@ content_types_accepted(Req, State) ->
Req, State}.
%%%% POST
--spec accept_json_modify_collection(cowboy_req:req(),#login_seq{})
+-spec accept_json_modify_collection(cowboy_req:req(),#state{})
-> {'true',cowboy_req:req(),_}.
accept_json_modify_collection(Req, Session) ->
case cowboy_req:has_body(Req) of
true ->
- {ok, Body, Req2} = read_body(Req),
+ {ok, Body, Req2} = ?UTILS:read_body(Req),
Parsed = jiffy:decode(Body, [return_maps]),
case Parsed of
#{ <<"verification_code">> := VerificationCode
@@ -61,7 +59,7 @@ accept_json_modify_collection(Req, Session) ->
Res1 = cowboy_req:set_resp_body(jiffy:encode(#{ success => false
, error => reason_to_json(Reason)
}), Req2),
- io:format("Error checking password reset code: ~p~n", [Reason]),
+ automate_logging:log_api(warning, ?MODULE, {error, Reason}),
{ false, Res1, Session}
end;
_ ->
@@ -78,13 +76,3 @@ reason_to_json({Type, Subtype}) ->
reason_to_json(Type) ->
#{ type => Type
}.
-
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_validate.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_validate.erl
index e3fe991d..47a57683 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_validate.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sessions_reset_password_validate.erl
@@ -10,17 +10,16 @@
]).
-export([accept_json_modify_collection/2]).
+
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
--record(login_seq, { rest_session,
- login_data
- }).
+-record(state, {}).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
{cowboy_rest, Req
- , #login_seq{ rest_session=undefined
- , login_data=undefined}}.
+ , #state{}}.
%% CORS
options(Req, State) ->
@@ -30,7 +29,6 @@ options(Req, State) ->
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
Res = automate_rest_api_cors:set_headers(Req),
- io:fwrite("[Password reset/Validate] Asking for methods~n", []),
{[<<"POST">>, <<"OPTIONS">>], Res, State}.
content_types_accepted(Req, State) ->
@@ -38,12 +36,12 @@ content_types_accepted(Req, State) ->
Req, State}.
%%%% POST
--spec accept_json_modify_collection(cowboy_req:req(),#login_seq{})
+-spec accept_json_modify_collection(cowboy_req:req(),#state{})
-> {'true',cowboy_req:req(),_}.
accept_json_modify_collection(Req, Session) ->
case cowboy_req:has_body(Req) of
true ->
- {ok, Body, Req2} = read_body(Req),
+ {ok, Body, Req2} = ?UTILS:read_body(Req),
Parsed = jiffy:decode(Body, [return_maps]),
case Parsed of
#{ <<"verification_code">> := VerificationCode} ->
@@ -59,7 +57,7 @@ accept_json_modify_collection(Req, Session) ->
Res1 = cowboy_req:set_resp_body(jiffy:encode(#{ success => false
, error => reason_to_json(Reason)
}), Req2),
- io:format("Error checking password reset code: ~p~n", [Reason]),
+ automate_logging:log_api(error, ?MODULE, Reason),
{ false, Res1, Session}
end;
_ ->
@@ -76,13 +74,3 @@ reason_to_json({Type, Subtype}) ->
reason_to_json(Type) ->
#{ type => Type
}.
-
-
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_sup.erl b/backend/apps/automate_rest_api/src/automate_rest_api_sup.erl
index 9e2fc5d7..ef9fd16b 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_sup.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_sup.erl
@@ -37,7 +37,6 @@ init([]) ->
, type => worker
, modules => [automate_rest_api_server]
}
-
]} }.
%%====================================================================
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_templates_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_templates_root.erl
index d9b7f1d5..737c6ca5 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_templates_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_templates_root.erl
@@ -16,10 +16,11 @@
, to_json/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
-include("../../automate_template_engine/src/records.hrl").
--record(state, { user_id }).
+-record(state, { user_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -43,7 +44,6 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
{[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
is_authorized(Req, State) ->
@@ -52,13 +52,17 @@ is_authorized(Req, State) ->
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+ Method ->
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
+ Scope = case Method of
+ <<"GET">> -> list_templates;
+ <<"POST">> -> create_templates
+ end,
#state{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
{true, UserId} ->
{ true, Req1, State };
{true, _} -> %% Non matching user id
@@ -79,11 +83,11 @@ content_types_accepted(Req, State) ->
accept_json_create_template(Req, State) ->
#state{user_id=UserId} = State,
- {ok, Body, Req1} = read_body(Req),
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
Template = jiffy:decode(Body, [return_maps]),
#{ <<"name">> := TemplateName, <<"content">> := TemplateContent } = Template,
- case automate_rest_api_backend:create_template(UserId, TemplateName, TemplateContent) of
+ case automate_rest_api_backend:create_template({user, UserId}, TemplateName, TemplateContent) of
{ ok, TemplateId } ->
Output = jiffy:encode(#{ <<"id">> => TemplateId
@@ -103,9 +107,8 @@ content_types_provided(Req, State) ->
-spec to_json(cowboy_req:req(), #state{})
-> {binary(),cowboy_req:req(), #state{}}.
-to_json(Req, State) ->
- #state{user_id=UserId} = State,
- case automate_rest_api_backend:list_templates_from_user_id(UserId) of
+to_json(Req, State=#state{user_id=UserId}) ->
+ case automate_template_engine:list_templates({user, UserId}) of
{ ok, Templates } ->
Output = jiffy:encode(lists:map(fun template_to_map/1, Templates)),
@@ -116,23 +119,13 @@ to_json(Req, State) ->
end.
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
-
-
-
template_to_map(#template_entry{ id=Id
, name=Name
- , owner=Owner
+ , owner={OwnerType, OwnerId}
, content=_Content
}) ->
#{ id => Id
, name => Name
- , owner => Owner
+ , owner => OwnerId
+ , owner_full => #{ type => OwnerType, id => OwnerId }
}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_templates_specific.erl b/backend/apps/automate_rest_api/src/automate_rest_api_templates_specific.erl
index 07420c98..f40e22fc 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_templates_specific.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_templates_specific.erl
@@ -16,10 +16,11 @@
, to_json/2
]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
-include("../../automate_template_engine/src/records.hrl").
--record(state, { user_id, template_id }).
+-record(state, { user_id :: binary(), template_id :: binary() }).
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, _Opts) ->
@@ -38,22 +39,27 @@ options(Req, State) ->
%% Authentication
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("[Template] Asking for methods~n", []),
{[<<"GET">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}.
-is_authorized(Req, State) ->
+is_authorized(Req, State=#state{ template_id=TemplateId }) ->
Req1 = automate_rest_api_cors:set_headers(Req),
case cowboy_req:method(Req1) of
%% Don't do authentication if it's just asking for options
<<"OPTIONS">> ->
{ true, Req1, State };
- _ ->
+ Method ->
case cowboy_req:header(<<"authorization">>, Req, undefined) of
undefined ->
{ {false, <<"Authorization header not found">>} , Req1, State };
X ->
#state{user_id=UserId} = State,
- case automate_rest_api_backend:is_valid_token_uid(X) of
+ Scope = case Method of
+ <<"GET">> -> { read_template, TemplateId };
+ <<"PUT">> -> { edit_template, TemplateId };
+ <<"DELETE">> -> { delete_template, TemplateId }
+
+ end,
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
{true, UserId} ->
{ true, Req1, State };
{true, _} -> %% Non matching user_id
@@ -66,7 +72,6 @@ is_authorized(Req, State) ->
content_types_accepted(Req, State) ->
- io:fwrite("[PUT] User > Template > ID~n", []),
{[{{<<"application">>, <<"json">>, []}, accept_json_template}],
Req, State}.
@@ -78,7 +83,6 @@ accept_json_template(Req, State) ->
%% Get handler
content_types_provided(Req, State) ->
- io:fwrite("[GET] User > Template > ID~n", []),
{[{{<<"application">>, <<"json">>, []}, to_json}],
Req, State}.
@@ -86,7 +90,7 @@ content_types_provided(Req, State) ->
-> {binary(),cowboy_req:req(), #state{}}.
to_json(Req, State) ->
#state{user_id=UserId, template_id=TemplateId} = State,
- case automate_rest_api_backend:get_template(UserId, TemplateId) of
+ case automate_rest_api_backend:get_template({user, UserId}, TemplateId) of
{ ok, Template } ->
Output = jiffy:encode(template_to_json(Template)),
@@ -98,7 +102,8 @@ to_json(Req, State) ->
{error, Reason} ->
Code = 500,
Output = jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }),
- cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req)
+ Res = cowboy_req:reply(Code, #{ <<"content-type">> => <<"application/json">> }, Output, Req),
+ { stop, Res, State }
end.
@@ -106,57 +111,39 @@ to_json(Req, State) ->
update_template(Req, State) ->
#state{template_id=TemplateId, user_id=UserId} = State,
- {ok, Body, Req1} = read_body(Req),
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
Parsed = jiffy:decode(Body, [return_maps]),
#{ <<"name">> := TemplateName, <<"content">> := TemplateContent } = Parsed,
- case automate_rest_api_backend:update_template(UserId, TemplateId, TemplateName, TemplateContent) of
+ case automate_rest_api_backend:update_template({user, UserId}, TemplateId, TemplateName, TemplateContent) of
ok ->
- Req2 = send_json_output(jiffy:encode(#{ <<"success">> => true }), Req),
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true }), Req),
{ true, Req2, State };
{ error, Reason } ->
- Req2 = send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req1),
+ Req2 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req1),
{ false, Req2, State }
end.
%% DELETE handler
delete_resource(Req, State) ->
#state{template_id=TemplateId, user_id=UserId} = State,
- case automate_rest_api_backend:delete_template(UserId, TemplateId) of
+ case automate_rest_api_backend:delete_template({user, UserId}, TemplateId) of
ok ->
- Req1 = send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => true}), Req),
{ true, Req1, State };
{ error, Reason } ->
- Req1 = send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req),
+ Req1 = ?UTILS:send_json_output(jiffy:encode(#{ <<"success">> => false, <<"message">> => Reason }), Req),
{ false, Req1, State }
end.
-
-%%%% Utils
-read_body(Req0) ->
- read_body(Req0, <<>>).
-
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
-
-
-send_json_output(Output, Req) ->
- Res1 = cowboy_req:set_resp_body(Output, Req),
- Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
- cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2).
-
-
-
template_to_json(#template_entry{ id=Id
, name=Name
- , owner=Owner
+ , owner={OwnerType, OwnerId}
, content=Content
}) ->
#{ id => Id
, name => Name
- , owner => Owner
+ , owner => OwnerId
+ , owner_full => #{ type => OwnerType, id => OwnerId }
, content => Content
}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_tokens_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_tokens_root.erl
new file mode 100644
index 00000000..21517c91
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_tokens_root.erl
@@ -0,0 +1,102 @@
+%%% @doc
+%%% REST endpoint to manage user tokens.
+%%% @end
+
+-module(automate_rest_api_tokens_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , content_types_accepted/2
+ , options/2
+ , is_authorized/2
+ ]).
+-export([accept_json/2]).
+
+-define(UTILS, automate_rest_api_utils).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() | undefined
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ {cowboy_rest, Req
+ , #state{ user_id=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, create_api_tokens) of
+ {true, UserId} ->
+ { true, Req1, #state{user_id=UserId} };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+%%%% POST
+-spec accept_json(cowboy_req:req(),#state{}) -> {'true',cowboy_req:req(),_}.
+accept_json(Req, Session=#state{ user_id=UserId }) ->
+ {ok, Body, Req2} = ?UTILS:read_body(Req),
+ #{ <<"scopes">> := ScopesStr } = jiffy:decode(Body, [return_maps]),
+ Scopes = parse_scopes(ScopesStr),
+ case automate_storage:generate_token_for_user(UserId, Scopes, never) of
+ { ok, Token } ->
+ Output = jiffy:encode(#{ success => true
+ , value => #{ token => Token
+ }
+ }),
+ Res1 = cowboy_req:set_resp_body(Output, Req2),
+ Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
+ Res3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2),
+ { true, Res3, Session };
+
+ {error, Reason} ->
+ Res1 = cowboy_req:set_resp_body(jiffy:encode(#{ success => false
+ , error => reason_to_json(Reason)
+ }), Req2),
+ automate_logging:log_api(error, ?MODULE, Reason),
+ { false, Res1, Session}
+ end.
+
+-spec parse_scopes(binary() | [binary()]) -> session_scope().
+parse_scopes(<<"all">>) ->
+ all;
+parse_scopes(Scopes) when is_list(Scopes) ->
+ lists:map(fun parse_single_scope/1, Scopes).
+
+parse_single_scope(<<"list_bridges">>) ->
+ list_bridges;
+parse_single_scope(<<"list_custom_blocks">>) ->
+ list_custom_blocks;
+parse_single_scope(<<"list_connections_established">>) ->
+ list_connections_established;
+parse_single_scope(<<"call_any_bridge">>) ->
+ call_any_bridge.
+
+reason_to_json(Reason) ->
+ list_to_binary(io_lib:format("~p", [Reason])).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_user_asset_by_id.erl b/backend/apps/automate_rest_api/src/automate_rest_api_user_asset_by_id.erl
new file mode 100644
index 00000000..cff04631
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_user_asset_by_id.erl
@@ -0,0 +1,91 @@
+-module(automate_rest_api_user_asset_by_id).
+-export([ init/2
+ , allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ , resource_exists/2
+ ]).
+-export([ retrieve_file/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-define(UTILS, automate_rest_api_utils).
+-define(MAX_AGE_IMMUTABLE_SECONDS, 31536000). %% Seconds in a year
+
+-record(state, { owner_id :: owner_id() | undefined
+ , asset_id :: binary()
+ , asset_info :: #user_asset_entry{} | undefined
+ }).
+
+-spec init(_, [user | group]) -> {'cowboy_rest',_,_}.
+init(Req, [OwnerType]) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ OwnerId = case OwnerType of
+ user ->
+ {user, cowboy_req:binding(user_id, Req)};
+ group ->
+ {group, cowboy_req:binding(group_id, Req)}
+ end,
+ AssetId = cowboy_req:binding(asset_id, Req1),
+ {cowboy_rest, Req1
+ , #state{ owner_id=OwnerId
+ , asset_id=AssetId
+ , asset_info=undefined
+ }}.
+
+resource_exists(Req, State=#state{owner_id=OwnerId, asset_id=AssetId}) ->
+ case automate_storage:get_user_asset_info(OwnerId, AssetId) of
+ {error, not_found} ->
+ {false, Req, State};
+ {ok, AssetInfo} ->
+ {true, Req, State#state{asset_info=AssetInfo}}
+ end.
+
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{owner_id=_OwnerId}) ->
+ case cowboy_req:method(Req) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req, State };
+ <<"GET">> ->
+ { true, Req, State }
+ end.
+
+
+%% Image handler
+content_types_provided(Req, State) ->
+ {[{{<<"octet">>, <<"stream">>, []}, retrieve_file}],
+ Req, State}.
+
+-spec retrieve_file(cowboy_req:req(), #state{}) -> {stop | boolean(),cowboy_req:req(), #state{}}.
+retrieve_file(Req, State=#state{ asset_id=AssetId
+ , owner_id=Owner
+ , asset_info=#user_asset_entry{mime_type=MimeType}
+ }) ->
+ Dir = ?UTILS:get_owner_asset_directory(Owner),
+ Path = list_to_binary([Dir, "/", AssetId]),
+ FileSize = filelib:file_size(Path),
+
+ ContentType = case MimeType of
+ { Type, undefined } ->
+ Type;
+ { Type, SubType } ->
+ list_to_binary([Type, "/", SubType])
+ end,
+
+ Res = cowboy_req:reply(200, #{ <<"content-type">> => ContentType
+ , <<"cache-control">> => list_to_binary(io_lib:fwrite("public, max-age=~p, immutable", [?MAX_AGE_IMMUTABLE_SECONDS]))
+ }, {sendfile, 0, FileSize, Path}, Req),
+ {stop, Res, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_user_assets.erl b/backend/apps/automate_rest_api/src/automate_rest_api_user_assets.erl
new file mode 100644
index 00000000..e35e8819
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_user_assets.erl
@@ -0,0 +1,118 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_user_assets).
+-export([ init/2
+ , allowed_methods/2
+ , content_types_provided/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ ]).
+-export([ accept_file/2
+ , list_files_to_json/2
+ ]).
+
+-include("../../automate_storage/src/records.hrl").
+-include("./records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { owner_id :: owner_id() | undefined
+ }).
+
+-spec init(_, [user | group]) -> {'cowboy_rest',_,_}.
+init(Req, [OwnerType]) ->
+ OwnerId = case OwnerType of
+ user ->
+ {user, cowboy_req:binding(user_id, Req)};
+ group ->
+ {group, cowboy_req:binding(group_id, Req)}
+ end,
+ {cowboy_rest, Req
+ , #state{ owner_id=OwnerId
+ }}.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{owner_id=OwnerId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ {Action, Scope} = case cowboy_req:method(Req1) of
+ <<"GET">> -> {can_user_view_as, list_assets};
+ _ -> {can_user_edit_as, create_assets}
+ end,
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UserId} ->
+ case automate_storage:Action({user, UserId}, OwnerId) of
+ true ->
+ { true, Req1, State };
+ false ->
+ { { false, <<"Action not authorized">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end
+ end
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"multipart">>, <<"form-data">>, []}, accept_file}],
+ Req, State}.
+
+-spec accept_file(cowboy_req:req(), #state{}) -> {boolean(),cowboy_req:req(), #state{}}.
+accept_file(Req, State=#state{owner_id=OwnerId}) ->
+ Path = ?UTILS:get_owner_asset_directory(OwnerId),
+ {ok, {AssetId, FileType}, Req1} = ?UTILS:stream_body_to_file_hashname(Req, Path, <<"file">>),
+
+ MimeType = case binary:split(FileType, <<"/">>) of
+ [Type, SubType] ->
+ {Type, SubType};
+ [Type] ->
+ {Type, undefined}
+ end,
+ ok = automate_storage:add_user_asset(OwnerId, AssetId, MimeType),
+
+ Output = jiffy:encode(#{ success => true
+ , value => AssetId
+ }),
+ Res2 = cowboy_req:set_resp_body(Output, Req1),
+ Res3 = cowboy_req:delete_resp_header(<<"content-type">>,
+ Res2),
+ Res4 = cowboy_req:set_resp_header(<<"content-type">>,
+ <<"application/json">>, Res3),
+ {true, Res4, State}.
+
+%% Image handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, list_files_to_json}],
+ Req, State}.
+
+-spec list_files_to_json(cowboy_req:req(), #state{}) -> {binary(),cowboy_req:req(), #state{}}.
+list_files_to_json(Req, State=#state{owner_id=OwnerId}) ->
+ {ok, Assets} = automate_storage:list_user_assets(OwnerId),
+
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { jiffy:encode(#{ success => true
+ , assets => ?FORMATTING:asset_list_to_json(Assets)
+ }), Res2, State }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_user_bridges_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_user_bridges_root.erl
new file mode 100644
index 00000000..58e159f2
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_user_bridges_root.erl
@@ -0,0 +1,126 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_user_bridges_root).
+
+-export([init/2]).
+
+-export([ allowed_methods/2
+ , content_types_accepted/2
+ , is_authorized/2
+ , options/2
+ , resource_exists/2
+ , content_types_provided/2
+ ]).
+
+-export([ accept_json_create_service_port/2
+ , to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(URLS, automate_rest_api_utils_urls).
+
+-record(state, {user_id:: binary()}).
+
+-spec init(_, _) -> {cowboy_rest, _, _}.
+
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ {cowboy_rest, Req,
+ #state{user_id=UserId}}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> -> {false, Req, State};
+ _ -> {true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(), _) -> {[binary()], cowboy_req:req(), _}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{user_id=UserId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> -> {true, Req1, State};
+ Method ->
+ case cowboy_req:header(<<"authorization">>, Req,
+ undefined)
+ of
+ undefined ->
+ {{false, <<"Authorization header not found">>}, Req1,
+ State};
+ X ->
+ Scope = case Method of
+ <<"GET">> -> list_bridges;
+ <<"POST">> -> create_bridges
+ end,
+ case automate_rest_api_backend:is_valid_token_uid(X, Scope) of
+ {true, UserId} -> {true, Req1, State};
+ {true, _} -> %% Non matching UserId
+ {{false, <<"Unauthorized">>},
+ Req1, State};
+ false ->
+ {{false, <<"Authorization not correct">>}, Req1, State}
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{user_id=UserId}) ->
+ case automate_service_port_engine:get_user_service_ports({user, UserId}) of
+ { ok, Bridges } ->
+ Output = jiffy:encode(lists:map(fun ?FORMATTING:bridge_to_json/1, Bridges)),
+
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ Res2 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1),
+
+ { Output, Res2, State }
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []},
+ accept_json_create_service_port}],
+ Req, State}.
+
+-spec accept_json_create_service_port(cowboy_req:req(),
+ #state{}) -> {{true,
+ binary()},
+ cowboy_req:req(),
+ #state{}}.
+
+accept_json_create_service_port(Req, State=#state{user_id=UserId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ #{ <<"name">> := ServicePortName } = jiffy:decode(Body, [return_maps]),
+
+ case automate_service_port_engine:create_service_port({user, UserId}, ServicePortName) of
+ {ok, ServicePortId} ->
+ ServicePortUrl = ?URLS:bridge_control_url(ServicePortId),
+ Output = jiffy:encode(#{ control_url => ServicePortUrl
+ , id => ServicePortId
+ }),
+ Res2 = cowboy_req:set_resp_body(Output, Req1),
+ Res3 = cowboy_req:delete_resp_header(<<"content-type">>,
+ Res2),
+ Res4 = cowboy_req:set_resp_header(<<"content-type">>,
+ <<"application/json">>, Res3),
+ {{true, ServicePortUrl}, Res4, State}
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_user_by_id_profile.erl b/backend/apps/automate_rest_api/src/automate_rest_api_user_by_id_profile.erl
new file mode 100644
index 00000000..6325a374
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_user_by_id_profile.erl
@@ -0,0 +1,84 @@
+%%% @doc
+%%% REST endpoint to manage user profile settings
+%%% @end
+
+-module(automate_rest_api_user_by_id_profile).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ , resource_exists/2
+ ]).
+
+-export([ accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+
+-record(state, { user_id :: binary() }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ {cowboy_rest, Req
+ , #state{ user_id=UserId }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ #state{user_id=UserId} = State,
+ case automate_rest_api_backend:is_valid_token_uid(X, edit_user_profile) of
+ {true, UserId} ->
+ { true, Req1, State };
+ {true, _} -> %% Non matching user id
+ { { false, <<"Unauthorized">>}, Req1, State };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+-spec accept_json(cowboy_req:req(), #state{}) -> {'true',cowboy_req:req(), #state{}}.
+accept_json(Req, State=#state{user_id=UserId}) ->
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ #{ <<"groups">> := Groups } = jiffy:decode(Body, [return_maps]),
+ case automate_storage:set_owner_public_listings({user, UserId}, Groups) of
+ ok ->
+ { true, ?UTILS:send_json_output(jiffy:encode(#{ success => true }), Req1), State };
+ {error, Reason} ->
+ automate_logging:log_api(error, ?MODULE, Reason),
+ { false, ?UTILS:send_json_output(jiffy:encode(#{ success => false }), Req1), State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_user_groups_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_user_groups_root.erl
new file mode 100644
index 00000000..3a9c5bf8
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_user_groups_root.erl
@@ -0,0 +1,76 @@
+%%% @doc
+%%% REST endpoint to list user groups.
+%%% @end
+
+-module(automate_rest_api_user_groups_root).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_id :: binary() }).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-define(UTILS, automate_rest_api_utils).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ {cowboy_rest, Req
+ , #state{ user_id=UserId }}.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ #state{user_id=UserId} = State,
+ case automate_rest_api_backend:is_valid_token_uid(X, list_groups) of
+ {true, UserId} ->
+ { true, Req1, State };
+ {true, _} -> %% Non matching username
+ { { false, <<"Unauthorized to create a program here">>}, Req1, State };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{user_id=UserId}) ->
+ case automate_storage:get_user_groups({user, UserId}) of
+ { ok, GroupRoles } ->
+ Output = jiffy:encode(#{ success => true, groups => lists:map(fun ?FORMATTING:group_and_role_to_json/1, GroupRoles)}),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_user_profile_by_name.erl b/backend/apps/automate_rest_api/src/automate_rest_api_user_profile_by_name.erl
new file mode 100644
index 00000000..fac3b369
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_user_profile_by_name.erl
@@ -0,0 +1,91 @@
+%%% @doc
+%%% REST endpoint to retrieve a user's profile.
+%%% @end
+
+-module(automate_rest_api_user_profile_by_name).
+-export([init/2]).
+-export([ allowed_methods/2
+ , content_types_provided/2
+ , options/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+
+-record(state, { user_name :: binary()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ UserName = cowboy_req:binding(user_name, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1, #state{ user_name=UserName }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{user_name=UserName}) ->
+ %% Get public profile
+ {ok, {user, UserId}} = automate_storage:get_userid_from_username(UserName),
+
+ {ok, Programs } = automate_storage:list_programs({user, UserId}),
+ {ok, Groups} = automate_storage:get_user_groups({user, UserId}),
+ {ListedGroups} = case automate_storage:get_owner_public_listings({user, UserId}) of
+ {ok, #user_profile_listings_entry{ groups=LGroups }} ->
+ { LGroups };
+ {error, not_found} ->
+ { [] }
+ end,
+
+ {ok, Bridges } = automate_service_port_engine:get_user_service_ports({user, UserId}),
+
+ ProgramList = lists:filtermap(fun(Program) ->
+ case Program of
+ #user_program_entry{ visibility=public } ->
+ ProgramBridges = try automate_bot_engine:get_bridges_on_program(Program) of
+ {ok, Result} ->
+ Result
+ catch ErrNS:Error:StackTrace ->
+ automate_logging:log_platform(error, ErrNS, Error, StackTrace),
+ []
+ end,
+ {true, ?FORMATTING:program_listing_to_json(Program, ProgramBridges)};
+ _ ->
+ false
+ end
+ end, Programs),
+
+ GroupList = lists:map(fun ?FORMATTING:group_and_role_to_json/1,
+ lists:filter(fun({#user_group_entry{ id=GroupId }, _}) ->
+ lists:any(fun(It) ->
+ GroupId == It
+ end, ListedGroups)
+ end, Groups)),
+
+ Output = jiffy:encode(#{ name => UserName
+ , id => UserId
+ , programs => ProgramList
+ , groups => GroupList
+ , bridges => lists:map(fun ?FORMATTING:bridge_to_json/1, Bridges)
+ }),
+ Res = ?UTILS:send_json_format(Req),
+
+ { Output, Res, State }.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_user_settings.erl b/backend/apps/automate_rest_api/src/automate_rest_api_user_settings.erl
new file mode 100644
index 00000000..d7032da5
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_user_settings.erl
@@ -0,0 +1,84 @@
+%%% @doc
+%%% REST endpoint to manage user settings
+%%% @end
+
+-module(automate_rest_api_user_settings).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ , resource_exists/2
+ ]).
+
+-export([ accept_json/2
+ ]).
+
+-define(UTILS, automate_rest_api_utils).
+-include("./records.hrl").
+
+-record(state, { user_id :: binary() }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ {cowboy_rest, Req
+ , #state{ user_id=UserId }}.
+
+resource_exists(Req, State) ->
+ case cowboy_req:method(Req) of
+ <<"POST">> ->
+ { false, Req, State };
+ _ ->
+ { true, Req, State}
+ end.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ #state{user_id=UserId} = State,
+ case automate_rest_api_backend:is_valid_token_uid(X, edit_user_settings) of
+ {true, UserId} ->
+ { true, Req1, State };
+ {true, _} -> %% Non matching user id
+ { { false, <<"Unauthorized">>}, Req1, State };
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, accept_json}],
+ Req, State}.
+
+-spec accept_json(cowboy_req:req(), #state{}) -> {'true',cowboy_req:req(), #state{}}.
+accept_json(Req, State) ->
+ #state{user_id=UserId} = State,
+ {ok, Body, Req1} = ?UTILS:read_body(Req),
+ case automate_storage:update_user_settings(UserId, jiffy:decode(Body, [return_maps]), [ user_permissions ]) of
+ ok ->
+ { true, ?UTILS:send_json_output(jiffy:encode(#{ success => true }), Req1), State };
+ {error, Reason} ->
+ automate_logging:log_api(error, ?MODULE, Reason),
+ { false, ?UTILS:send_json_output(jiffy:encode(#{ success => false }), Req1), State }
+ end.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_users_picture.erl b/backend/apps/automate_rest_api/src/automate_rest_api_users_picture.erl
new file mode 100644
index 00000000..006888af
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_users_picture.erl
@@ -0,0 +1,101 @@
+%%% @doc
+%%% REST endpoint to manage knowledge collections.
+%%% @end
+
+-module(automate_rest_api_users_picture).
+-export([ init/2
+ , allowed_methods/2
+ , content_types_provided/2
+ , options/2
+ , is_authorized/2
+ , content_types_accepted/2
+ , resource_exists/2
+ , last_modified/2
+ ]).
+-export([ accept_file/2
+ , retrieve_file/2
+ ]).
+
+-include("./records.hrl").
+-define(UTILS, automate_rest_api_utils).
+
+-record(state, { user_id :: binary()
+ , last_modification_time :: undefined | calendar:datetime()
+ }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ UserId = cowboy_req:binding(user_id, Req),
+ {cowboy_rest, Req
+ , #state{ user_id=UserId
+ , last_modification_time=undefined
+ }}.
+
+resource_exists(Req, State=#state{user_id=UserId}) ->
+ case ?UTILS:user_picture_modification_time(UserId) of
+ {error, not_found} ->
+ {false, Req, State};
+ { ok, ModTime }->
+ {true, Req, State#state{ last_modification_time=ModTime }}
+ end.
+
+last_modified(Req, State=#state{last_modification_time=ModTime}) ->
+ {ModTime, Req, State}.
+
+%% CORS
+options(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"POST">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{user_id=UserId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ <<"GET">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, edit_user_picture) of
+ {true, UserId} ->
+ { true, Req1, State };
+ false ->
+ { { false, <<"Unauthorized">>}, Req1, State }
+ end
+ end
+ end.
+
+%% POST handler
+content_types_accepted(Req, State) ->
+ {[{{<<"multipart">>, <<"form-data">>, []}, accept_file}],
+ Req, State}.
+
+-spec accept_file(cowboy_req:req(), #state{}) -> {boolean(),cowboy_req:req(), #state{}}.
+accept_file(Req, State=#state{user_id=UserId}) ->
+ Path = ?UTILS:user_picture_path(UserId),
+ {ok, _Data, Req1} = ?UTILS:stream_body_to_file(Req, Path, <<"file">>),
+ {true, Req1, State}.
+
+
+%% Image handler
+content_types_provided(Req, State) ->
+ {[{{<<"octet">>, <<"stream">>, []}, retrieve_file}],
+ Req, State}.
+
+-spec retrieve_file(cowboy_req:req(), #state{}) -> {stop | boolean(),cowboy_req:req(), #state{}}.
+retrieve_file(Req, State=#state{user_id=UserId}) ->
+ Path = ?UTILS:user_picture_path(UserId),
+ FileSize = filelib:file_size(Path),
+
+ Res = cowboy_req:reply(200, #{ %% <<"content-type">> => "image/png"
+ }, {sendfile, 0, FileSize, Path}, Req),
+ {stop, Res, State}.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_users_root.erl b/backend/apps/automate_rest_api/src/automate_rest_api_users_root.erl
index d13eea17..4d6b5bee 100644
--- a/backend/apps/automate_rest_api/src/automate_rest_api_users_root.erl
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_users_root.erl
@@ -5,58 +5,102 @@
-module(automate_rest_api_users_root).
-export([init/2]).
-export([ allowed_methods/2
- , content_types_accepted/2
+ , is_authorized/2
+ , content_types_provided/2
, options/2
]).
-%% -export([is_authorized/2]).
--export([accept_json_modify_collection/2]).
-%% -include("include/records.hrl").
+-export([ to_json/2
+ ]).
+-define(UTILS, automate_rest_api_utils).
-include("./records.hrl").
-
+-include("../../automate_storage/src/records.hrl").
-spec init(_,_) -> {'cowboy_rest',_,_}.
init(Req, Opts) ->
{cowboy_rest, Req, Opts}.
-%% -spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
-%% is_authorized(Req, State) ->
-%% rest_is_authorized:is_authorized(Req, State).
+-spec is_authorized(cowboy_req:req(),_) -> {'true' | {'false', binary()}, cowboy_req:req(),_}.
+is_authorized(Req, State) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+ _ ->
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ { {false, <<"Authorization header not found">>} , Req1, State };
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, admin_list_users) of
+ {true, UserId} ->
+ case automate_storage:get_user(UserId) of
+ {ok, #registered_user_entry{ is_admin=true }} ->
+ { true, Req1, State };
+ {ok, _} ->
+ { { false, <<"User not authorized (not admin)">>}, Req1, State };
+ {error, Reason} ->
+ automage_logging:log_api(error, ?MODULE, Reason)
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
%% CORS
options(Req, State) ->
- Req1 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"GET, POST, OPTIONS">>, Req),
- Req2 = cowboy_req:set_resp_header(<<"access-control-allow-origin">>, <<"*">>, Req1),
- {ok, Req2, State}.
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {ok, Req1, State}.
-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
allowed_methods(Req, State) ->
- io:fwrite("Asking for methods~n", []),
- {[<<"POST">>, <<"GET">>, <<"OPTIONS">>], Req, State}.
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
-content_types_accepted(Req, State) ->
- {[{{<<"application">>, <<"json">>, []}, accept_json_modify_collection}],
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
Req, State}.
-%%%% POST
- %
--spec accept_json_modify_collection(cowboy_req:req(),#rest_session{}) -> {'true',cowboy_req:req(),_}.
-accept_json_modify_collection(Req, Session) ->
- case cowboy_req:has_body(Req) of
- true ->
- {ok, Body, Req2} = read_body(Req),
- io:fwrite("--->~p ~n", [Body]),
- io:fwrite("-+->~p ~n", [jiffy:decode(Body, [return_maps])]),
- {true, Req2, Session};
- false ->
- {false, Req, Session }
- end.
+%%%% GET
+-spec to_json(cowboy_req:req(),#rest_session{}) -> {binary(),cowboy_req:req(),_}.
+to_json(Req, Session) ->
+ {ok, Result} = automate_storage:admin_list_users(),
+ Output = jiffy:encode(lists:map(fun(U) -> full_serialize_user(U) end, Result)),
+ Res = ?UTILS:send_json_format(Req),
+ { Output, Res, Session }.
-read_body(Req0) ->
- read_body(Req0, <<>>).
-read_body(Req0, Acc) ->
- case cowboy_req:read_body(Req0) of
- {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
- {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
- end.
+%% Users
+full_serialize_user({#registered_user_entry{ id=UserId
+ , username=_Username
+ , canonical_username=CanonicalUsername
+ , password=_
+ , email=Email
+ , status=Status
+ , registration_time=RegistrationTime
+
+ , is_admin=IsAdmin
+ , is_advanced=IsAdvanced
+ , is_in_preview=IsInPreview
+ }
+ , LastActiveTime
+ }) ->
+ #{ success => true
+ , username => CanonicalUsername
+ , user_id => UserId
+ , email => Email
+ , status => Status
+ , registration_time => number_or_undefined(RegistrationTime)
+ , last_active_time => number_or_undefined(LastActiveTime)
+ , tags => #{ is_admin => IsAdmin
+ , is_advanced => IsAdvanced
+ , is_in_preview => IsInPreview
+ }
+ }.
+
+number_or_undefined(undefined) ->
+ null;
+number_or_undefined(none) ->
+ null;
+number_or_undefined(Num) ->
+ Num.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_utils.erl b/backend/apps/automate_rest_api/src/automate_rest_api_utils.erl
new file mode 100644
index 00000000..85414026
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_utils.erl
@@ -0,0 +1,253 @@
+-module(automate_rest_api_utils).
+-export([ read_body/1
+ , send_json_output/2
+ , send_json_format/1
+ , stream_body_to_file/3
+ , stream_body_to_file_hashname/3
+ , copy_asset/3
+ , user_picture_path/1
+ , user_has_picture/1
+ , user_picture_modification_time/1
+ , group_picture_path/1
+ , group_has_picture/1
+ , group_picture_modification_time/1
+ , get_bridges_on_program_id/1
+ , get_owner_asset_directory/1
+ , is_public/1
+
+ , start_metrics/2
+ , end_metrics/1
+ , end_metrics_with_error/2
+ ]).
+
+-include("../../automate_common_types/src/types.hrl").
+-define(HASH_ALGORITHM, sha3_256).
+
+read_body(Req0) ->
+ read_body(Req0, <<>>).
+
+read_body(Req0, Acc) ->
+ case cowboy_req:read_body(Req0) of
+ {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req};
+ {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>)
+ end.
+
+send_json_output(Output, Req) ->
+ Res1 = cowboy_req:set_resp_body(Output, Req),
+ Res2 = cowboy_req:delete_resp_header(<<"content-type">>, Res1),
+ cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res2).
+
+send_json_format(Req) ->
+ Res1 = cowboy_req:delete_resp_header(<<"content-type">>, Req),
+ cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Res1).
+
+stream_body_to_file(Req, Path, FileName) ->
+ TmpPath = tmp_path(),
+ ok = filelib:ensure_dir(TmpPath),
+ {ok, File} = file:open(TmpPath, [write, raw]),
+ try multipart(Req, File, #{}, FileName, nohash) of
+ Res ->
+ ok = filelib:ensure_dir(Path),
+ ok = movefile(TmpPath, Path),
+ Res
+ after
+ case filelib:is_file(TmpPath) of
+ true ->
+ ok = file:close(File),
+ ok = file:delete(TmpPath);
+ false ->
+ ok
+ end
+ end.
+
+stream_body_to_file_hashname(Req, Path, FileKey) ->
+ TmpPath = tmp_path(),
+ ok = filelib:ensure_dir(TmpPath),
+ {ok, File} = file:open(TmpPath, [write, raw]),
+ try multipart(Req, File, #{}, FileKey, hash) of
+ {ok, #{ FileKey := {Hash, FileType} }, Req1} ->
+ Id = url_safe_base64_encode(Hash),
+ FullPath = list_to_binary(io_lib:format("~s/~s", [Path, Id])),
+ ok = filelib:ensure_dir(FullPath),
+ ok = movefile(TmpPath, FullPath),
+ {ok, {Id, FileType}, Req1}
+ after
+ case filelib:is_file(TmpPath) of
+ true ->
+ ok = file:close(File),
+ ok = file:delete(TmpPath);
+ false ->
+ ok
+ end
+ end.
+
+copy_asset(FromOwner, ToOwner, AssetId) ->
+ FromDir = get_owner_asset_directory(FromOwner),
+ FromPath = list_to_binary([FromDir, "/", AssetId]),
+ ToDir = get_owner_asset_directory(ToOwner),
+ ToPath = list_to_binary([ToDir, "/", AssetId]),
+
+ ok = filelib:ensure_dir(ToPath),
+
+ {ok, _BytesCopied} = file:copy(FromPath, ToPath),
+ ok.
+
+user_has_picture(UserId) ->
+ filelib:is_file(user_picture_path(UserId)).
+
+user_picture_modification_time(UserId) ->
+ case filelib:last_modified(user_picture_path(UserId)) of
+ 0 -> {error, not_found};
+ Date -> { ok, Date }
+ end.
+
+-spec user_picture_path(binary()) -> binary().
+user_picture_path(UserId) ->
+ binary:list_to_bin(
+ lists:flatten(io_lib:format("~s/~s/picture", [automate_configuration:asset_directory("public/users/")
+ , UserId
+ ]))).
+
+group_has_picture(GroupId) ->
+ filelib:is_file(group_picture_path(GroupId)).
+
+-spec group_picture_path(binary()) -> binary().
+group_picture_path(GroupId) ->
+ binary:list_to_bin(
+ lists:flatten(io_lib:format("~s/~s/picture", [automate_configuration:asset_directory("public/groups/")
+ , GroupId
+ ]))).
+
+group_picture_modification_time(GroupId) ->
+ case filelib:last_modified(group_picture_path(GroupId)) of
+ 0 -> {error, not_found};
+ Date -> { ok, Date }
+ end.
+
+-spec get_owner_asset_directory(owner_id()) -> binary().
+get_owner_asset_directory({OwnerType, OwnerId}) ->
+ OwnerTypeDir = case OwnerType of
+ user -> "users";
+ group -> "groups"
+ end,
+ list_to_binary(io_lib:format("~s/~s/_assets",
+ [automate_configuration:asset_directory("public/" ++ OwnerTypeDir ++ "/" )
+ , OwnerId
+ ])).
+
+
+-spec is_public(user_program_visibility()) -> boolean().
+is_public(Visibility) ->
+ case Visibility of
+ public -> true;
+ shareable -> true;
+ private -> false
+ end.
+%% Auxiliary
+movefile(Source, Target) ->
+ case file:rename(Source, Target) of
+ ok ->
+ ok;
+ {error, exdev} -> %% Source and target on different devices
+ {ok, _BytesCopied} = file:copy(Source, Target),
+ ok = file:delete(Source)
+ end.
+
+tmp_path() ->
+ BackupName = atom_to_list(?MODULE) ++ "/" ++ integer_to_list(erlang:phash2(make_ref())),
+ BackupDir = filename:basedir(user_cache, "automate"),
+ BackupDir ++ "/" ++ BackupName.
+
+multipart(Req0, File, Data, ToSave, Options) ->
+ case cowboy_req:read_part(Req0) of
+ {ok, Headers, Req} ->
+ {ReqCont, NewData} = case cow_multipart:form_data(Headers) of
+ {data, FieldName} ->
+ {ok, Body, Req2} = cowboy_req:read_part_body(Req),
+ {Req2, Data#{ FieldName => Body }};
+ {file, ToSave, _FileName, FileType} ->
+ InfoAcc = case Options of
+ nohash -> 0;
+ hash -> {hash, crypto:hash_init(?HASH_ALGORITHM)}
+ end,
+ {ok, Req2, Result} = stream_body_content_to_file(Req, File, InfoAcc),
+ {Req2, Data#{ ToSave => {Result, FileType} }};
+ {file, _, _, _} ->
+ {Req, Data}
+ end,
+ multipart(ReqCont, File, NewData, ToSave, Options);
+ {done, Req} ->
+ {ok, Data, Req}
+ end.
+
+stream_body_content_to_file(Req0, File, Acc) ->
+ case cowboy_req:read_part_body(Req0) of
+ {ok, Data, Req} ->
+ ok = file:write(File, Data),
+ NextData = case Acc of
+ X when is_number(X) -> X + size(Data);
+ {hash, HashAcc} ->
+ crypto:hash_final(crypto:hash_update(HashAcc, Data))
+ end,
+ {ok, Req, NextData};
+ {more, Data, Req} ->
+ ok = file:write(File, Data),
+ NextData = case Acc of
+ X when is_number(X) -> X + size(Data);
+ {hash, HashAcc} ->
+ {hash, crypto:hash_update(HashAcc, Data)}
+ end,
+ stream_body_content_to_file(Req, File, NextData)
+ end.
+
+get_bridges_on_program_id(ProgramId) ->
+ {ok, Program} = automate_storage:get_program_from_id(ProgramId),
+ {ok, Bridges} = automate_bot_engine:get_bridges_on_program(Program),
+ Bridges.
+
+%% Note that this is not going to be decoded (although the replacements are reversible)
+url_safe_base64_encode(Bin) ->
+ RawB64 = base64:encode(Bin),
+ %% Replace elements with specific URL semantics
+ S1 = binary:replace(RawB64, <<"/">>, <<"-">>, [global]),
+ S2 = binary:replace(S1, <<"+">>, <<"_">>, [global]),
+ S2.
+
+%%====================================================================
+%% Metrics
+%%====================================================================
+-type user_agent_bucket() :: google_apps_script | other.
+-record(metrics_data, { user_agent_bucket :: user_agent_bucket()
+ , start_time :: integer()
+ , endpoint :: atom()
+ }).
+
+get_user_agent_bucket(undefined) ->
+ other;
+get_user_agent_bucket(UserAgent) ->
+ case binary:matches(UserAgent, <<"Google-Apps-Script">>) of
+ [] -> other;
+ _ -> google_apps_script
+ end.
+
+start_metrics(Req, Endpoint) ->
+ #metrics_data{ user_agent_bucket=get_user_agent_bucket(cowboy_req:header(<<"user-agent">>, Req))
+ , start_time=erlang:monotonic_time()
+ , endpoint=Endpoint
+ }.
+
+end_metrics(#metrics_data{ user_agent_bucket=Bucket
+ , start_time=StartTime
+ , endpoint=Endpoint
+ }) ->
+ EndTime = erlang:monotonic_time(),
+ TimeElapsed = erlang:convert_time_unit(EndTime - StartTime, native, millisecond),
+ prometheus_histogram:observe(default, automate_api_latency, [Endpoint, Bucket, ok], TimeElapsed).
+
+end_metrics_with_error(#metrics_data{ user_agent_bucket=Bucket
+ , start_time=StartTime
+ , endpoint=Endpoint
+ }, Error) ->
+ EndTime = erlang:monotonic_time(),
+ TimeElapsed = erlang:convert_time_unit(EndTime - StartTime, native, millisecond),
+ prometheus_histogram:observe(default, automate_api_latency, [Endpoint, Bucket, Error], TimeElapsed).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_utils_formatting.erl b/backend/apps/automate_rest_api/src/automate_rest_api_utils_formatting.erl
new file mode 100644
index 00000000..4cb5c8fa
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_utils_formatting.erl
@@ -0,0 +1,370 @@
+-module(automate_rest_api_utils_formatting).
+
+-export([ format_message/1
+ , serialize_logs/2
+ , serialize_log_entry/1
+ , serialize_variable_map/1
+ , serialize_icon/1
+ , serialize_maybe_undefined/1
+ , reason_to_json/1
+ , group_to_json/1
+ , group_and_role_to_json/1
+ , program_listing_to_json/1
+ , program_listing_to_json/2
+ , program_data_to_json/2
+ , collaborator_to_json/1
+ , user_to_json/1
+ , bridge_to_json/1
+ , connection_to_json/1
+ , asset_list_to_json/1
+ , serialize_event_error/1
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-include("../../automate_bot_engine/src/program_records.hrl").
+-include("../../automate_service_port_engine/src/records.hrl").
+
+-define(UTILS, automate_rest_api_utils).
+
+format_message(Log=#user_program_log_entry{}) ->
+ {ok, #{ type => program_log
+ , value => serialize_log_entry(Log)
+ }};
+format_message(Log=#user_generated_log_entry{}) ->
+ {ok, #{ type => debug_log
+ , value => serialize_log_entry(Log)
+ }};
+format_message(_) ->
+ {error, unknown_format}.
+
+
+serialize_logs(ErrorLogs, UserLogs) ->
+ lists:sort(fun (#{ event_time := A }, #{ event_time := B }) ->
+ A < B
+ end,
+ lists:map(fun (Entry) -> serialize_log_entry(Entry) end, ErrorLogs)
+ ++ lists:map(fun (Entry) -> serialize_log_entry(Entry) end, UserLogs)).
+
+serialize_log_entry(#user_program_log_entry{ program_id=ProgramId
+ , thread_id=ThreadId
+ , owner=Owner
+ , block_id=BlockId
+ , event_data=EventData
+ , event_message=EventMessage
+ , event_time=EventTime
+ , severity=Severity
+ , exception_data=_ExceptionData
+ }) ->
+ {OwnerType, OwnerId} = case Owner of
+ { Type, Id } -> {Type, Id};
+ Id ->
+ automate_logging:log_platform(migration_warning,
+ io_lib:format("Unfinished migration. Found bare user Id on program: ~p", [Id])),
+ { user, Id }
+ end,
+
+ #{ program_id => ProgramId
+ , thread_id => serialize_string_or_none(ThreadId)
+ , owner => #{ type => OwnerType, id => serialize_string_or_none(OwnerId) }
+ , user_id => serialize_string_or_none(OwnerId)
+ , block_id => serialize_string_or_none(BlockId)
+ , event_data => serialize_event_error(EventData)
+ , event_message => EventMessage
+ , event_time => EventTime
+ , severity => Severity
+ };
+
+serialize_log_entry(#user_generated_log_entry{ program_id=ProgramId
+ , block_id=BlockId
+ , event_message=EventMessage
+ , event_time=EventTime
+ , severity=Severity
+ }) ->
+ #{ program_id => ProgramId
+ , thread_id => null
+ , owner => null
+ , user_id => null
+ , block_id => serialize_string_or_none(BlockId)
+ , event_data => debug
+ , event_message => EventMessage
+ , event_time => EventTime
+ , severity => Severity
+ }.
+
+serialize_variable_map(VariableMap) ->
+ maps:filter(fun(K, _V) ->
+ is_binary(K) or is_list(K)
+ end, VariableMap).
+
+serialize_string_or_none(none) ->
+ null;
+serialize_string_or_none(String) ->
+ String.
+
+serialize_event_error(#program_error{ error=Error
+ , block_id=BlockId
+ }) ->
+ #{ error => serialize_error_subtype(Error)
+ , block_id => BlockId
+ };
+serialize_event_error(_) ->
+ unknown_error.
+
+-spec serialize_error_subtype(program_error_type()) -> map().
+serialize_error_subtype(#variable_not_set{variable_name=VariableName}) ->
+ #{ type => variable_not_set
+ , variable_name => VariableName
+ };
+
+serialize_error_subtype(#memory_not_set{block_id=BlockId}) ->
+ #{ type => memory_not_set
+ , block_id => BlockId
+ };
+
+serialize_error_subtype(#list_not_set{list_name=ListName}) ->
+ #{ type => list_not_set
+ , list_name => ListName
+ };
+
+serialize_error_subtype(#index_not_in_list{ list_name=ListName
+ , index=Index
+ , max=MaxIndex
+ }) ->
+ #{ type => index_not_in_list
+ , list_name => ListName
+ , index => Index
+ , length => MaxIndex
+ };
+
+serialize_error_subtype(#invalid_list_index_type{ list_name=ListName
+ , index=Index
+ }) ->
+ #{ type => invalid_list_index_type
+ , list_name => ListName
+ , index => Index
+ };
+
+serialize_error_subtype(#disconnected_bridge{ bridge_id=BridgeId
+ , action=Action
+ }) ->
+ #{ type => disconnected_bridge
+ , bridge_id => BridgeId
+ , action => Action
+ };
+
+serialize_error_subtype(#bridge_call_connection_not_found{ bridge_id=BridgeId
+ , action=Action
+ }) ->
+ #{ type => bridge_call_connection_not_found
+ , bridge_id => BridgeId
+ , action => Action
+ };
+
+serialize_error_subtype(#bridge_call_timeout{ bridge_id=BridgeId
+ , action=Action
+ }) ->
+ #{ type => bridge_call_timeout
+ , bridge_id => BridgeId
+ , action => Action
+ };
+
+serialize_error_subtype(#bridge_call_failed{ bridge_id=BridgeId
+ , action=Action
+ , reason=Reason
+ }) ->
+ #{ type => bridge_call_failed
+ , bridge_id => BridgeId
+ , action => Action
+ , reason => serialize_maybe_undefined(Reason)
+ };
+
+serialize_error_subtype(#bridge_call_error_getting_resource{ bridge_id=BridgeId
+ , action=Action
+ }) ->
+ #{ type => bridge_call_error_getting_resource
+ , bridge_id => BridgeId
+ , action => Action
+ };
+
+serialize_error_subtype(#unknown_operation{}) ->
+ #{ type => unknown_operation
+ }.
+
+
+serialize_icon(undefined) ->
+ null;
+serialize_icon({url, Url}) ->
+ #{ <<"url">> => Url };
+serialize_icon({hash, HashType, HashResult}) ->
+ #{ HashType => HashResult }.
+
+serialize_maybe_undefined(undefined) ->
+ null;
+serialize_maybe_undefined(X) ->
+ X.
+
+%% Reason
+reason_to_json({Type, Subtype}) ->
+ #{ type => list_to_binary(io_lib:format("~p", [Type]))
+ , subtype => list_to_binary(io_lib:format("~p", [Subtype]))
+ };
+reason_to_json(Type) ->
+ #{ type => list_to_binary(io_lib:format("~p", [Type]))
+ }.
+
+group_to_json(#user_group_entry{ id=Id
+ , name=Name
+ , canonical_name=CanonicalName
+ , public=IsPublic
+ , min_level_for_private_bridge_usage=MinLevelForPrivateBridgeUsage
+ }) ->
+ Picture = case ?UTILS:group_has_picture(Id) of
+ false -> null;
+ true ->
+ <<"/groups/by-id/", Id/binary, "/picture">>
+ end,
+ #{ id => Id
+ , name => Name
+ , public => IsPublic
+ , canonical_name => CanonicalName
+ , picture => Picture
+ , min_level_for_private_bridge_usage => MinLevelForPrivateBridgeUsage
+ }.
+
+
+group_and_role_to_json({Group, Role}) ->
+ GroupJson = group_to_json(Group),
+ GroupJson#{ role => Role }.
+
+program_listing_to_json(#user_program_entry{ id=Id
+ , program_name=Name
+ , enabled=Enabled
+ , program_type=Type
+ , visibility=Visibility
+ }) ->
+ #{ id => Id
+ , name => Name
+ , enabled => Enabled
+ , type => Type
+ , is_public => ?UTILS:is_public(Visibility)
+ , visibility => Visibility
+ };
+program_listing_to_json(#program_metadata{ id=Id
+ , name=Name
+ , enabled=Enabled
+ , type=Type
+ , visibility=Visibility
+ }) ->
+ #{ id => Id
+ , name => Name
+ , enabled => Enabled
+ , type => Type
+ , is_public => ?UTILS:is_public(Visibility)
+ , visibility => Visibility
+ }.
+
+program_listing_to_json(Program, Bridges) ->
+ Base = program_listing_to_json(Program),
+ Base#{ bridges_in_use => Bridges }.
+
+
+program_data_to_json(#user_program{ id=Id
+ , owner=Owner=#{ id := OwnerId}
+ , program_name=ProgramName
+ , program_type=ProgramType
+ , program_parsed=ProgramParsed
+ , program_orig=ProgramOrig
+ , enabled=Enabled
+ , visibility=Visibility
+ },
+ Checkpoint) ->
+ #{ <<"id">> => Id
+ , <<"owner">> => OwnerId
+ , <<"owner_full">> => Owner
+ , <<"name">> => ProgramName
+ , <<"type">> => ProgramType
+ , <<"parsed">> => ProgramParsed
+ , <<"orig">> => ProgramOrig
+ , <<"enabled">> => Enabled
+ , <<"checkpoint">> => Checkpoint
+ , <<"is_public">> => ?UTILS:is_public(Visibility)
+ , <<"visibility">> => Visibility
+ }.
+
+
+user_to_json(#registered_user_entry{ id=Id
+ , username=Username
+ }) ->
+ Picture = case ?UTILS:user_has_picture(Id) of
+ false -> null;
+ true ->
+ <<"/users/by-id/", Id/binary, "/picture">>
+ end,
+ #{ id => Id
+ , username => Username
+ , picture => Picture
+ }.
+
+collaborator_to_json({ User , Role
+ }) ->
+ UserDict = user_to_json(User),
+ UserDict#{ role => Role }.
+
+
+bridge_to_json(#service_port_entry_extra{ id=Id
+ , name=Name
+ , owner={OwnerType, OwnerId}
+ , is_connected=IsConnected
+ , icon=Icon
+ }) ->
+ #{ <<"id">> => Id
+ , <<"name">> => Name
+ , <<"owner">> => OwnerId
+ , <<"owner_full">> => #{type => OwnerType, id => OwnerId}
+ , <<"is_connected">> => IsConnected
+ , <<"icon">> => serialize_icon(Icon)
+ }.
+
+-spec connection_to_json(#user_to_bridge_connection_entry{}) -> false | {true, map()}.
+connection_to_json(#user_to_bridge_connection_entry{ id=Id
+ , bridge_id=BridgeId
+ , owner=_
+ , channel_id=_
+ , name=Name
+ , creation_time=_CreationTime
+ , save_signals=Saving
+ }) ->
+ case automate_service_port_engine:get_bridge_info(BridgeId) of
+ {ok, #service_port_metadata{ name=BridgeName, icon=Icon }} ->
+ {true, #{ <<"connection_id">> => Id
+ , <<"name">> => serialize_string_or_undefined(Name)
+ , <<"bridge_id">> => BridgeId
+ , <<"bridge_name">> => serialize_string_or_undefined(BridgeName)
+ , <<"icon">> => serialize_icon(Icon)
+ , <<"saving">> => Saving
+ } };
+ {error, _Reason} ->
+ false
+ end.
+
+-spec asset_list_to_json([#user_asset_entry{}]) -> [map()].
+asset_list_to_json(Assets) ->
+ lists:map(fun(#user_asset_entry{ asset_id={ {OwnerType, OwnerId}, AssetId }, mime_type=MimeType }) ->
+ #{ id => AssetId
+ , owner_full => #{ type => OwnerType
+ , id => OwnerId
+ }
+ , mime_type => case MimeType of
+ { Type, undefined } ->
+ Type;
+ { Type, Subtype } ->
+ <>
+ end
+ }
+ end, Assets).
+
+serialize_string_or_undefined(undefined) ->
+ null;
+serialize_string_or_undefined(String) ->
+ String.
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_utils_programs.erl b/backend/apps/automate_rest_api/src/automate_rest_api_utils_programs.erl
new file mode 100644
index 00000000..7028e66f
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_utils_programs.erl
@@ -0,0 +1,38 @@
+-module(automate_rest_api_utils_programs).
+
+-export([ get_metadata_from_body/1
+ ]).
+
+-include("records.hrl").
+
+get_metadata_from_body(Body) ->
+ Map = jiffy:decode(Body, [return_maps]),
+ { get_program_type_from_options(Map)
+ , get_program_name_from_options(Map)
+ }.
+
+
+%% Util functions
+get_program_type_from_options(#{ <<"type">> := <<"scratch_program">> }) ->
+ scratch_program;
+get_program_type_from_options(#{ <<"type">> := <<"flow_program">> }) ->
+ flow_program;
+get_program_type_from_options(#{ <<"type">> := <<"spreadsheet_program">> }) ->
+ spreadsheet_program;
+get_program_type_from_options(#{ <<"type">> := Type }) when is_binary(Type) ->
+ Type;
+get_program_type_from_options(_) ->
+ ?DEFAULT_PROGRAM_TYPE.
+
+get_program_name_from_options(#{ <<"name">> := Name }) when is_binary(Name) ->
+ case size(Name) < 4 of
+ true ->
+ generate_program_name();
+ false ->
+ Name
+ end;
+get_program_name_from_options(_) ->
+ generate_program_name().
+
+generate_program_name() ->
+ binary:list_to_bin(uuid:to_string(uuid:uuid4())).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_utils_urls.erl b/backend/apps/automate_rest_api/src/automate_rest_api_utils_urls.erl
new file mode 100644
index 00000000..a46fa952
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_utils_urls.erl
@@ -0,0 +1,17 @@
+-module(automate_rest_api_utils_urls).
+-export([ service_id_url/1
+ , bridge_control_url/1
+ , bridge_token_by_name_url/2
+ ]).
+
+-spec service_id_url(binary()) -> binary().
+service_id_url(ServiceId) ->
+ binary:list_to_bin(lists:flatten(io_lib:format("/api/v0/services/by-id/~s", [ServiceId]))).
+
+
+bridge_control_url(BridgeId) ->
+ binary:list_to_bin(lists:flatten(io_lib:format("/api/v0/bridges/by-id/~s/communication", [BridgeId]))).
+
+
+bridge_token_by_name_url(BridgeId, TokenName) ->
+ io_lib:format("/api/v0/bridges/by-id/~s/token/by-name/~s", [BridgeId, TokenName]).
diff --git a/backend/apps/automate_rest_api/src/automate_rest_api_validate_connection_token_by_program_id.erl b/backend/apps/automate_rest_api/src/automate_rest_api_validate_connection_token_by_program_id.erl
new file mode 100644
index 00000000..8624c026
--- /dev/null
+++ b/backend/apps/automate_rest_api/src/automate_rest_api_validate_connection_token_by_program_id.erl
@@ -0,0 +1,87 @@
+%%% @doc
+%%% REST endpoint to validate connection tokens to programs.
+%%% @end
+
+-module(automate_rest_api_validate_connection_token_by_program_id).
+-export([init/2]).
+-export([ allowed_methods/2
+ , options/2
+ , is_authorized/2
+ , content_types_provided/2
+ ]).
+
+-export([ to_json/2
+ ]).
+
+-include("./records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-define(UTILS, automate_rest_api_utils).
+-define(FORMATTING, automate_rest_api_utils_formatting).
+
+-record(state, { program_id :: binary(), user_id :: binary() | undefined }).
+
+-spec init(_,_) -> {'cowboy_rest',_,_}.
+init(Req, _Opts) ->
+ ProgramId = cowboy_req:binding(program_id, Req),
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ {cowboy_rest, Req1
+ , #state{ program_id=ProgramId
+ , user_id=undefined
+ }}.
+
+%% CORS
+options(Req, State) ->
+ {ok, Req, State}.
+
+%% Authentication
+-spec allowed_methods(cowboy_req:req(),_) -> {[binary()], cowboy_req:req(),_}.
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"OPTIONS">>], Req, State}.
+
+is_authorized(Req, State=#state{program_id=ProgramId}) ->
+ Req1 = automate_rest_api_cors:set_headers(Req),
+ case cowboy_req:method(Req1) of
+ %% Don't do authentication if it's just asking for options
+ <<"OPTIONS">> ->
+ { true, Req1, State };
+
+ %% Reading a public program
+ Method ->
+ {ok, #user_program_entry{ visibility=Visibility }} = automate_storage:get_program_from_id(ProgramId),
+ IsPublic = ?UTILS:is_public(Visibility),
+ Action = edit_program,
+ case cowboy_req:header(<<"authorization">>, Req, undefined) of
+ undefined ->
+ case {Method, IsPublic} of
+ {<<"GET">>, true} ->
+ { true, Req1, State };
+ _ ->
+ { {false, <<"Authorization header not found">>} , Req1, State }
+ end;
+ X ->
+ case automate_rest_api_backend:is_valid_token_uid(X, {read_program, ProgramId}) of
+ {true, UId} ->
+ case automate_storage:is_user_allowed({user, UId}, ProgramId, Action) of
+ {ok, true} ->
+ { true, Req1, State#state{user_id=UId} };
+ {ok, false} ->
+ { { false, <<"Action not authorized">>}, Req1, State };
+ {error, Reason} ->
+ automate_logging:log_api(warning, ?MODULE, {authorization_error, Reason}),
+ { { false, <<"Error on authorization">>}, Req1, State }
+ end;
+ false ->
+ { { false, <<"Authorization not correct">>}, Req1, State }
+ end
+ end
+ end.
+
+%% GET handler
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, to_json}],
+ Req, State}.
+
+-spec to_json(cowboy_req:req(), #state{})
+ -> {binary(),cowboy_req:req(), #state{}}.
+to_json(Req, State=#state{program_id=_ProgramId, user_id=UserId}) ->
+ { jiffy:encode(#{ success => true, user_id => UserId }), Req, State }.
diff --git a/backend/apps/automate_rest_api/src/records.hrl b/backend/apps/automate_rest_api/src/records.hrl
index a0099405..a49a7f7a 100644
--- a/backend/apps/automate_rest_api/src/records.hrl
+++ b/backend/apps/automate_rest_api/src/records.hrl
@@ -1,3 +1,6 @@
+-define(DEFAULT_PROGRAM_TYPE, scratch_program).
+-include("../../automate_common_types/src/types.hrl").
+
-record(rest_session,
{ user_id
, session_id
@@ -14,24 +17,28 @@
, username
}).
--record(user_program, { id
- , user_id
- , program_name
- , program_type
- , program_parsed
- , program_orig
- , enabled
+-record(user_program, { id :: binary()
+ , owner :: #{ type => (user | group), id => binary() }
+ , program_name :: binary()
+ , program_type :: binary() | atom()
+ , program_parsed :: any()
+ , program_orig :: any()
+ , enabled :: boolean()
+ , last_upload_time :: integer()
+ , visibility :: user_program_visibility()
}).
--record(program_metadata, { id
- , name
- , link
- , enabled
+-record(program_metadata, { id :: binary()
+ , name :: binary()
+ , enabled :: boolean()
+ , type :: boolean()
+ , visibility :: user_program_visibility()
}).
-record(program_content, { type
, orig
, parsed
+ , pages = #{} :: map()
}).
-record(monitor_metadata, { id
diff --git a/backend/apps/automate_rest_api/test/automate_rest_api_bridge_connection_tests.erl b/backend/apps/automate_rest_api/test/automate_rest_api_bridge_connection_tests.erl
new file mode 100644
index 00000000..1ea4c091
--- /dev/null
+++ b/backend/apps/automate_rest_api/test/automate_rest_api_bridge_connection_tests.erl
@@ -0,0 +1,289 @@
+%%% Automate bridge token creation, API tests.
+%%% @end
+
+-module(automate_rest_api_bridge_connection_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+
+-define(APPLICATION, automate_rest_api).
+-define(PREFIX, "automate_rest_api_token_tests_").
+-define(RECEIVE_TIMEOUT, 100).
+-define(BRIDGE_BACKEND, automate_service_port_engine_mnesia_backend).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ Port = get_test_port(),
+ application:set_env(automate_rest_api, port, Port, [{persistent, true}]),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _} = application:ensure_all_started(automate_storage),
+ {ok, _} = application:ensure_all_started(automate_service_port_engine),
+ {ok, _} = application:ensure_all_started(automate_channel_engine),
+ {ok, _} = application:ensure_all_started(?APPLICATION),
+
+ {Port, NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_Port, _NodeName}) ->
+ ok = application:stop(?APPLICATION),
+
+ ok.
+
+
+tests({Port, _}) ->
+ [ { "[API - Bridges - Detect when connection is established] Detect connection and disconnection of bridge"
+ , fun() -> detect_bridge_connection_disconnection(Port) end
+ }
+ , { "[API - Bridges - Detect when connection is established] Interlocking connection-disconnection"
+ , fun() -> detect_bridge_connection_disconnection_interlocking(Port) end
+ }
+ ].
+
+
+%%====================================================================
+%% Tests
+%%====================================================================
+detect_bridge_connection_disconnection(Port) ->
+ #{ user_id := UserId
+ , bridge_id := BridgeId
+ , bridge_token := BridgeToken
+ } = create_bridge_and_token(Port),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => "Bridge connection detection test - 1"
+ , <<"blocks">> => [ ]
+ },
+
+ ok = automate_service_port_engine:from_service_port(BridgeId, {user, UserId},
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, _ConnectionId} = establish_connection(BridgeId, {user, UserId}),
+
+ ok = automate_service_registry_query:listen_service(BridgeId, {user, UserId}, {undefined, undefined}),
+
+ AuthMessage = jiffy:encode(#{ <<"type">> => <<"AUTHENTICATION">>
+ , <<"value">> => #{ <<"token">> => BridgeToken
+ }
+ }),
+
+ %% Connect
+ {ok, AuthState={state, _, _, _, true}} = automate_rest_api_service_ports_specific_communication:websocket_handle(
+ {text, AuthMessage}, { state, {user, UserId}, BridgeId, #{}, false }),
+
+ receive
+ { channel_engine
+ , _ChannelId
+ , #{ <<"key">> := '__proto_on_bridge_connected'
+ , <<"service_id">> := BridgeId
+ , <<"subkey">> := BridgeId
+ , <<"value">> := <<"connected">>
+ }
+ } ->
+ ok
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+
+ %% Disconnect
+ ok = automate_rest_api_service_ports_specific_communication:terminate(test, idk, AuthState),
+
+ receive
+ { channel_engine
+ , _ChannelId2
+ , #{ <<"key">> := '__proto_on_bridge_disconnected'
+ , <<"service_id">> := BridgeId
+ , <<"subkey">> := BridgeId
+ , <<"value">> := <<"disconnected">>
+ }
+ } ->
+ ok
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+
+ ok.
+
+detect_bridge_connection_disconnection_interlocking(Port) ->
+ #{ user_id := UserId
+ , bridge_id := BridgeId
+ , bridge_token := BridgeToken
+ } = create_bridge_and_token(Port),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => "Bridge connection detection test - 1"
+ , <<"blocks">> => [ ]
+ },
+
+ ok = automate_service_port_engine:from_service_port(BridgeId, {user, UserId},
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, _ConnectionId} = establish_connection(BridgeId, {user, UserId}),
+
+ ok = automate_service_registry_query:listen_service(BridgeId, {user, UserId}, {undefined, undefined}),
+
+ AuthMessage = jiffy:encode(#{ <<"type">> => <<"AUTHENTICATION">>
+ , <<"value">> => #{ <<"token">> => BridgeToken
+ }
+ }),
+
+ %% Connections are handled on different processes so the router can differentiate them
+ %% First connection, on a different PID so the router can differentiate between the two
+ Conn1 = spawn(
+ fun() ->
+ {ok, AuthState2={state, _, _, _, true}} = automate_rest_api_service_ports_specific_communication:websocket_handle(
+ {text, AuthMessage}, { state, {user, UserId}, BridgeId, #{}, false }),
+ receive continue -> ok end,
+ %% Disconnect
+ ok = automate_rest_api_service_ports_specific_communication:terminate(test, idk, AuthState2)
+ end),
+
+
+ %% First connection triggers a connection event
+ receive
+ { channel_engine
+ , _ChannelId
+ , #{ <<"key">> := '__proto_on_bridge_connected'
+ , <<"service_id">> := BridgeId
+ , <<"subkey">> := BridgeId
+ , <<"value">> := <<"connected">>
+ }
+ } ->
+ ok
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+
+ %% Second connection, on a different PID so the router can differentiate between the two
+ Conn2 = spawn(
+ fun() ->
+ {ok, AuthState2={state, _, _, _, true}} = automate_rest_api_service_ports_specific_communication:websocket_handle(
+ {text, AuthMessage}, { state, {user, UserId}, BridgeId, #{}, false }),
+ receive continue -> ok end,
+ %% Disconnect
+ ok = automate_rest_api_service_ports_specific_communication:terminate(test, idk, AuthState2)
+ end),
+
+ %% Second connnection does NOT trigger a connection event
+ receive
+ { channel_engine
+ , _ChannelId2
+ , #{ <<"key">> := '__proto_on_bridge_connected'
+ , <<"service_id">> := BridgeId
+ , <<"subkey">> := BridgeId
+ , <<"value">> := <<"connected">>
+ }
+ } ->
+ ct:fail(should_not_happen)
+ after ?RECEIVE_TIMEOUT ->
+ ok
+ end,
+
+ Conn1 ! continue,
+
+ %% First disconnection does NOT trigger a disconnection event
+ receive
+ { channel_engine
+ , _ChannelId3
+ , #{ <<"key">> := '__proto_on_bridge_disconnected'
+ , <<"service_id">> := BridgeId
+ , <<"subkey">> := BridgeId
+ , <<"value">> := <<"disconnected">>
+ }
+ } ->
+ ct:fail(should_not_happen)
+ after ?RECEIVE_TIMEOUT ->
+ ok
+ end,
+
+ %% Second disconnection
+ Conn2 ! continue,
+
+ %% Second disconnection does trigger a disconnection event
+ receive
+ { channel_engine
+ , _ChannelId4
+ , #{ <<"key">> := '__proto_on_bridge_disconnected'
+ , <<"service_id">> := BridgeId
+ , <<"subkey">> := BridgeId
+ , <<"value">> := <<"disconnected">>
+ }
+ } ->
+ ok
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+
+ ok.
+
+
+%%====================================================================
+%% Utils
+%%====================================================================
+create_bridge_and_token(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({user, UserId}, BridgeName),
+ {ok, {UserToken, UserId} } = automate_storage:login_user(Username, Password),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens", [ Port, BridgeId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(post
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(UserToken) } ]
+ , "application/json"
+ , jiffy:encode(#{ name => TokenName })
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {{_, 201, _}, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ #{ <<"key">> := BridgeToken } = jiffy:decode(Body, [return_maps]),
+ #{ user_id => UserId
+ , bridge_id => BridgeId
+ , bridge_token => BridgeToken
+ }.
+
+fmt_list(FmtStr, Params) ->
+ lists:flatten(io_lib:format(FmtStr, Params)).
+
+get_test_port() ->
+ 12345.
+
+establish_connection(BridgeId, UserId) ->
+ case ?BRIDGE_BACKEND:gen_pending_connection(BridgeId, UserId) of
+ {ok, ConnectionId} ->
+ ok = ?BRIDGE_BACKEND:establish_connection(BridgeId, UserId, ConnectionId, <<"test connection">>),
+ {ok, ConnectionId};
+ {error, Reason} ->
+ {error, Reason}
+ end.
diff --git a/backend/apps/automate_rest_api/test/automate_rest_api_token_tests.erl b/backend/apps/automate_rest_api/test/automate_rest_api_token_tests.erl
new file mode 100644
index 00000000..5865da0f
--- /dev/null
+++ b/backend/apps/automate_rest_api/test/automate_rest_api_token_tests.erl
@@ -0,0 +1,477 @@
+%%% Automate bridge token creation, API tests.
+%%% @end
+
+-module(automate_rest_api_token_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+
+-define(APPLICATION, automate_rest_api).
+-define(PREFIX, "automate_rest_api_token_tests_").
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ Port = get_test_port(),
+ application:set_env(automate_rest_api, port, Port, [{persistent, true}]),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _} = application:ensure_all_started(?APPLICATION),
+
+ {Port, NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_Port, _NodeName}) ->
+ ok = application:stop(?APPLICATION),
+
+ ok.
+
+
+tests({Port, _}) ->
+ %% Token creation
+ [ { "[Token API][Creation] Simple token creation", fun() -> simple_token_creation(Port) end }
+ , { "[Token API][Creation] Different user cannot create token", fun() -> different_user_cannot_create_token(Port) end }
+ , { "[Token API][Creation] Create token on group bridge", fun() -> create_token_on_group_bridge(Port) end }
+ , { "[Token API][Creation] Different user cannot create token on group bridge", fun() -> different_user_cannot_create_token_on_group_bridge(Port) end }
+
+ %% Token listing
+ , { "[Token API][Listing] Create token and list", fun() -> create_token_and_list(Port) end }
+ , { "[Token API][Listing] Create token and different user cannot list", fun() -> create_token_and_different_user_cannot_list(Port) end }
+ , { "[Token API][Listing] Create token and list on group", fun() -> create_token_and_list_on_group(Port) end }
+ , { "[Token API][Listing] Create token and list on group without permissions", fun() -> create_token_and_list_on_group_no_permission(Port) end }
+
+ %% Token revocation
+ , { "[Token API][Revocation] Simple token creation and revocation", fun() -> simple_token_revocation(Port) end }
+ , { "[Token API][Revocation] Different user cannot revoke token", fun() -> different_user_cannot_revoke_token(Port) end }
+ , { "[Token API][Revocation] Create token on group bridge and revoke it", fun() -> revoke_token_on_group_bridge(Port) end }
+ , { "[Token API][Revocation] Different user cannot revoke token on group bridge", fun() -> different_user_cannot_revoke_token_on_group_bridge(Port) end }
+ ].
+
+
+%%====================================================================
+%% Tests
+%%====================================================================
+simple_token_creation(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({user, UserId}, BridgeName),
+ {ok, {UserToken, UserId} } = automate_storage:login_user(Username, Password),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens", [ Port, BridgeId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(post
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(UserToken) } ]
+ , "application/json"
+ , jiffy:encode(#{ name => TokenName })
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+
+ ?assertMatch({_, 201, _StatusMessage}, Status).
+
+
+different_user_cannot_create_token(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Username = <>,
+ Password = <>,
+ Mail = <>,
+ BridgeName = <>,
+
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({user, UserId}, BridgeName),
+
+ DiffUsername = <>,
+ DiffPassword = <>,
+ DiffMail = <>,
+
+ {ok, DiffUserId} = automate_storage:create_user(DiffUsername, DiffPassword, DiffMail, ready),
+ {ok, {DiffUserToken, DiffUserId} } = automate_storage:login_user(DiffUsername, DiffPassword),
+
+ TokenName = <<"simple-token">>,
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens", [ Port, BridgeId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+
+ Result = httpc:request(post
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(DiffUserToken) } ]
+ , "application/json"
+ , jiffy:encode(#{ name => TokenName })
+ }
+ , [], []),
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+
+ ?assertMatch({_, 401, _StatusMessage}, Status).
+
+
+create_token_on_group_bridge(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ GroupName = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, {UserToken, UserId} } = automate_storage:login_user(Username, Password),
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, UserId, false),
+
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({group, GroupId}, BridgeName),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens?as_group=~s", [ Port, BridgeId, GroupId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(post
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(UserToken) } ]
+ , "application/json"
+ , jiffy:encode(#{ name => TokenName })
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+
+ ?assertMatch({_, 201, _StatusMessage}, Status).
+
+
+different_user_cannot_create_token_on_group_bridge(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ GroupName = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, UserId, false),
+
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({group, GroupId}, BridgeName),
+
+ DiffUsername = <>,
+ DiffPassword = <>,
+ DiffMail = <>,
+
+ {ok, DiffUserId} = automate_storage:create_user(DiffUsername, DiffPassword, DiffMail, ready),
+ {ok, {DiffUserToken, DiffUserId} } = automate_storage:login_user(DiffUsername, DiffPassword),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens?as_group=~s", [ Port, BridgeId, GroupId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(post
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(DiffUserToken) } ]
+ , "application/json"
+ , jiffy:encode(#{ name => TokenName })
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+
+ ?assertMatch({_, 401, _StatusMessage}, Status).
+
+
+create_token_and_list(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({user, UserId}, BridgeName),
+ {ok, {UserToken, UserId} } = automate_storage:login_user(Username, Password),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {user, UserId}, TokenName, undefined),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens", [ Port, BridgeId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(get
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(UserToken) } ]
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 200, _StatusMessage}, Status),
+
+ Contents = jiffy:decode(Body, [return_maps]),
+ ?assertMatch(#{ <<"tokens">> := [#{ <<"name">> := TokenName }] }, Contents).
+
+
+create_token_and_different_user_cannot_list(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({user, UserId}, BridgeName),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {user, UserId}, TokenName, undefined),
+
+ DiffUsername = <>,
+ DiffPassword = <>,
+ DiffMail = <>,
+ {ok, DiffUserId} = automate_storage:create_user(DiffUsername, DiffPassword, DiffMail, ready),
+ {ok, {DiffUserToken, DiffUserId} } = automate_storage:login_user(DiffUsername, DiffPassword),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens", [ Port, BridgeId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(get
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(DiffUserToken) } ]
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 401, _StatusMessage}, Status).
+
+
+create_token_and_list_on_group(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ GroupName = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, {UserToken, UserId} } = automate_storage:login_user(Username, Password),
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, UserId, false),
+
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({group, GroupId}, BridgeName),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {group, GroupId}, TokenName, undefined),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens?as_group=~s", [ Port, BridgeId, GroupId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(get
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(UserToken) } ]
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 200, _StatusMessage}, Status),
+
+ Contents = jiffy:decode(Body, [return_maps]),
+ ?assertMatch(#{ <<"tokens">> := [#{ <<"name">> := TokenName }] }, Contents).
+
+
+create_token_and_list_on_group_no_permission(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ GroupName = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, UserId, false),
+
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({group, GroupId}, BridgeName),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {group, GroupId}, TokenName, undefined),
+
+ DiffUsername = <>,
+ DiffPassword = <>,
+ DiffMail = <>,
+
+ {ok, DiffUserId} = automate_storage:create_user(DiffUsername, DiffPassword, DiffMail, ready),
+ {ok, {DiffUserToken, DiffUserId} } = automate_storage:login_user(DiffUsername, DiffPassword),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens?as_group=~s", [ Port, BridgeId, GroupId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request(post
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(DiffUserToken) } ]
+ , "application/json"
+ , jiffy:encode(#{ name => TokenName })
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 401, _StatusMessage}, Status).
+
+
+simple_token_revocation(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({user, UserId}, BridgeName),
+ {ok, {UserToken, UserId} } = automate_storage:login_user(Username, Password),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {user, UserId}, TokenName, undefined),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens/by-name/~s", [ Port, BridgeId, TokenName ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request( delete
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(UserToken) } ]
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 200, _StatusMessage}, Status),
+
+ ?assertMatch({ok, []}, automate_service_port_engine:list_bridge_tokens(BridgeId)).
+
+
+different_user_cannot_revoke_token(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({user, UserId}, BridgeName),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {user, UserId}, TokenName, undefined),
+
+ DiffUsername = <>,
+ DiffPassword = <>,
+ DiffMail = <>,
+
+ {ok, DiffUserId} = automate_storage:create_user(DiffUsername, DiffPassword, DiffMail, ready),
+ {ok, {DiffUserToken, DiffUserId} } = automate_storage:login_user(DiffUsername, DiffPassword),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens/by-name/~s", [ Port, BridgeId, TokenName ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request( delete
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(DiffUserToken) } ]
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 401, _StatusMessage}, Status).
+
+
+revoke_token_on_group_bridge(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ GroupName = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, {UserToken, UserId} } = automate_storage:login_user(Username, Password),
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, UserId, false),
+
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({group, GroupId}, BridgeName),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {group, GroupId}, TokenName, undefined),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens/by-name/~s?as_group=~s", [ Port, BridgeId, TokenName, GroupId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request( delete
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(UserToken) } ]
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 200, _StatusMessage}, Status),
+
+ ?assertMatch({ok, []}, automate_service_port_engine:list_bridge_tokens(BridgeId)).
+
+
+different_user_cannot_revoke_token_on_group_bridge(Port) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ TokenName = <<"simple-token">>,
+ Username = <>,
+ Password = <>,
+ GroupName = <>,
+ Mail = <>,
+ BridgeName = <>,
+ {ok, UserId} = automate_storage:create_user(Username, Password, Mail, ready),
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, UserId, false),
+
+ {ok, BridgeId} = automate_service_port_engine:create_service_port({group, GroupId}, BridgeName),
+ {ok, _TokenKey} = automate_service_port_engine:create_bridge_token(BridgeId, {group, GroupId}, TokenName, undefined),
+
+ DiffUsername = <>,
+ DiffPassword = <>,
+ DiffMail = <>,
+
+ {ok, DiffUserId} = automate_storage:create_user(DiffUsername, DiffPassword, DiffMail, ready),
+ {ok, {DiffUserToken, DiffUserId} } = automate_storage:login_user(DiffUsername, DiffPassword),
+
+ Uri = fmt_list("http://localhost:~p/api/v0/bridges/by-id/~s/tokens/by-name/~s?as_group=~s", [ Port, BridgeId, TokenName, GroupId ]),
+ io:fwrite("Uri: ~p~n", [Uri]),
+ Result = httpc:request( delete
+ , { Uri
+ , [ { "Authorization", binary:bin_to_list(DiffUserToken) } ]
+ }
+ , [], []),
+
+ io:fwrite("Result: ~p~n", [Result]),
+ {ok, {Status, _Headers, Body}} = Result,
+
+ io:fwrite("Body: ~p~n", [Body]),
+ ?assertMatch({_, 401, _StatusMessage}, Status).
+
+
+%%====================================================================
+%% Utils
+%%====================================================================
+fmt_list(FmtStr, Params) ->
+ lists:flatten(io_lib:format(FmtStr, Params)).
+
+get_test_port() ->
+ 12345.
diff --git a/backend/apps/automate_rest_api/test/automate_rest_api_user_registration.erl b/backend/apps/automate_rest_api/test/automate_rest_api_user_registration.erl
new file mode 100644
index 00000000..151a22b3
--- /dev/null
+++ b/backend/apps/automate_rest_api/test/automate_rest_api_user_registration.erl
@@ -0,0 +1,178 @@
+%%% Automate bridge token creation, API tests.
+%%% @end
+
+-module(automate_rest_api_user_registration).
+-include_lib("eunit/include/eunit.hrl").
+
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+
+-define(APPLICATION, automate_rest_api).
+-define(PREFIX, "api_reg_test_").
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ Port = get_test_port(),
+ application:set_env(automate_rest_api, port, Port, [{persistent, true}]),
+
+ MailTab = ets:new(rest_api_user_registration_tests, [ordered_set, public]),
+ application:set_env(automate_mail, mail_gateway, { test_ets, MailTab }, [{persistent, true}]),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _} = application:ensure_all_started(automate_storage),
+ {ok, _} = application:ensure_all_started(automate_channel_engine),
+ {ok, _} = application:ensure_all_started(?APPLICATION),
+
+
+ {Port, MailTab, NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_Port, MailTab, _NodeName}) ->
+ ets:delete(MailTab),
+ ok = application:stop(?APPLICATION),
+
+ ok.
+
+
+tests({Port, MailTab, _}) ->
+ %% User registration
+ [ { "[User registration] Simple operative", fun() -> simple_registration(Port, MailTab) end }
+ , { "[User registration] Handle on verify twice", fun() -> handle_verify_twice(Port, MailTab) end }
+ ].
+
+
+%%====================================================================
+%% Tests
+%%====================================================================
+simple_registration(Port, MailTab) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Username = binary:replace(<>, <<"-">>, <<"_">>, [global]),
+ Password = <>,
+ Mail = <>,
+
+ RegUri = fmt_list("http://localhost:~p/api/v0/sessions/register", [ Port ]),
+ io:fwrite("RegisterUri: ~p~n", [RegUri]),
+ RegResult = httpc:request(post
+ , { RegUri
+ , [ ]
+ , "application/json"
+ , jiffy:encode(#{ email => Mail
+ , username => Username
+ , password => Password
+ })
+ }
+ , [], []),
+
+ io:fwrite("RegisterResult: ~p~n", [RegResult]),
+ {ok, {RegStatus, _RegHeaders, RegBody}} = RegResult,
+
+ io:fwrite("RegisterBody: ~p~n", [RegBody]),
+ ?assertMatch({_, 200, _RegStatusMessage}, RegStatus),
+
+ [{Mail, Username, VerificationCode, _}] = ets:lookup(MailTab, Mail),
+
+ VerifyUri = fmt_list("http://localhost:~p/api/v0/sessions/register/verify", [ Port ]),
+ io:fwrite("VerifyUri: ~p~n", [VerifyUri]),
+ VerifyResult = httpc:request(post
+ , { VerifyUri
+ , [ ]
+ , "application/json"
+ , jiffy:encode(#{ verification_code => VerificationCode
+ })
+ }
+ , [], []),
+
+ io:fwrite("VerifyResult: ~p~n", [VerifyResult]),
+ {ok, {VerifyStatus, _VerifyHeaders, VerifyBody}} = VerifyResult,
+
+ io:fwrite("VerifyBody: ~p~n", [VerifyBody]),
+ ?assertMatch({_, 200, _VerifyStatusMessage}, VerifyStatus),
+
+ ok.
+
+handle_verify_twice(Port, MailTab) ->
+ Uuid = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Username = binary:replace(<>, <<"-">>, <<"_">>, [global]),
+ Password = <>,
+ Mail = <>,
+
+ RegUri = fmt_list("http://localhost:~p/api/v0/sessions/register", [ Port ]),
+ io:fwrite("RegisterUri: ~p~n", [RegUri]),
+ RegResult = httpc:request(post
+ , { RegUri
+ , [ ]
+ , "application/json"
+ , jiffy:encode(#{ email => Mail
+ , username => Username
+ , password => Password
+ })
+ }
+ , [], []),
+
+ io:fwrite("RegisterResult: ~p~n", [RegResult]),
+ {ok, {RegStatus, _RegHeaders, RegBody}} = RegResult,
+
+ io:fwrite("RegisterBody: ~p~n", [RegBody]),
+ ?assertMatch({_, 200, _RegStatusMessage}, RegStatus),
+
+ [{Mail, Username, VerificationCode, _}] = ets:lookup(MailTab, Mail),
+
+ Verify = fun() ->
+ VerifyUri = fmt_list("http://localhost:~p/api/v0/sessions/register/verify", [ Port ]),
+ io:fwrite("VerifyUri: ~p~n", [VerifyUri]),
+ VerifyResult = httpc:request(post
+ , { VerifyUri
+ , [ ]
+ , "application/json"
+ , jiffy:encode(#{ verification_code => VerificationCode
+ })
+ }
+ , [], []),
+
+ io:fwrite("VerifyResult: ~p~n", [VerifyResult]),
+ {ok, {VerifyStatus, _VerifyHeaders, VerifyBody}} = VerifyResult,
+
+ io:fwrite("VerifyBody: ~p~n", [VerifyBody]),
+ {_, 200, _VerifyStatusMessage} = VerifyStatus,
+ #{ <<"success">> := true
+ , <<"session">> := #{ <<"token">> := Token
+ , <<"user_id">> := _
+ , <<"username">> := Username
+ }
+ } = jiffy:decode(VerifyBody, [return_maps]),
+ ?assert(size(Token) > 4)
+ end,
+
+ Verify(),
+
+ io:fwrite("Going for the second...~n"),
+
+ Verify(),
+
+ ok.
+
+%%====================================================================
+%% Utils
+%%====================================================================
+fmt_list(FmtStr, Params) ->
+ lists:flatten(io_lib:format(FmtStr, Params)).
+
+get_test_port() ->
+ 12345.
diff --git a/backend/apps/automate_service_port_engine/src/automate_service_port_engine.app.src b/backend/apps/automate_service_port_engine/src/automate_service_port_engine.app.src
index 48efdb0e..fb5dbb93 100644
--- a/backend/apps/automate_service_port_engine/src/automate_service_port_engine.app.src
+++ b/backend/apps/automate_service_port_engine/src/automate_service_port_engine.app.src
@@ -12,6 +12,8 @@
, automate_stats
, automate_storage
, automate_configuration
+ , uuid
+ , eargon2
]},
{env, [
]},
diff --git a/backend/apps/automate_service_port_engine/src/automate_service_port_engine.erl b/backend/apps/automate_service_port_engine/src/automate_service_port_engine.erl
index 96f40f25..73e3b466 100644
--- a/backend/apps/automate_service_port_engine/src/automate_service_port_engine.erl
+++ b/backend/apps/automate_service_port_engine/src/automate_service_port_engine.erl
@@ -9,6 +9,7 @@
%% Application callbacks
-export([ create_service_port/2
, register_service_port/1
+ , unregister_service_port/1
, from_service_port/3
, call_service_port/5
, get_how_to_enable/2
@@ -16,13 +17,37 @@
, send_oauth_return/2
, list_custom_blocks/1
- , internal_user_id_to_service_port_user_id/2
+ , internal_user_id_to_connection_id/2
, get_user_service_ports/1
, delete_bridge/2
- , callback_bridge/3
+ , callback_bridge/4
+ , callback_bridge_through_connection/4
, get_channel_origin_bridge/1
+ , get_bridge_info/1
+ , get_bridge_owner/1
+ , get_bridge_configuration/1
+ , is_bridge_connected/1
, listen_bridge/2
+ , listen_bridge/3
+ , list_established_connections/1
+ , list_established_connections/2
+ , get_pending_connection_info/1
+ , is_module_connectable_bridge/2
+
+ , set_shared_resource/3
+ , get_connection_owner/1
+ , get_connection_shares/1
+ , get_connection_bridge/1
+ , get_resources_shared_with/1
+ , get_resources_shared_with_on_bridge/2
+
+ , create_bridge_token/4
+ , list_bridge_tokens/1
+ , delete_bridge_token_by_name/2
+ , check_bridge_token/2
+ , can_skip_authentication/1
+ , set_save_signals_on_connection/3
]).
-include("records.hrl").
@@ -36,36 +61,49 @@
%% API
%%====================================================================
--spec create_service_port(binary(), binary()) -> {ok, binary()} | {error, term(), string()}.
-create_service_port(UserId, ServicePortName) ->
- ?BACKEND:create_service_port(UserId, ServicePortName).
+-spec create_service_port(owner_id(), binary()) -> {ok, binary()} | {error, term(), string()}.
+create_service_port(Owner, ServicePortName) when is_tuple(Owner) ->
+ ?BACKEND:create_service_port(Owner, ServicePortName).
-spec register_service_port(binary()) -> ok.
register_service_port(ServicePortId) ->
?ROUTER:connect_bridge(ServicePortId).
--spec call_service_port(binary(), binary(), any(), binary(), map()) -> {ok, map()} | {error, ?ROUTER_ERROR_CLASSES}.
-call_service_port(ServicePortId, FunctionName, Arguments, UserId, ExtraData) ->
- ?LOGGING:log_call_to_bridge(ServicePortId,FunctionName,Arguments,UserId,ExtraData),
+-spec unregister_service_port(binary()) -> ok.
+unregister_service_port(ServicePortId) ->
+ ?ROUTER:disconnect_bridge(ServicePortId).
+
+-spec call_service_port(binary(), binary(), any(), owner_id() | binary(), map()) -> {ok, map()} | {error, ?ROUTER_ERROR_CLASSES}.
+call_service_port(ServicePortId, FunctionName, Arguments, Owner, ExtraData) when is_tuple(Owner) ->
+ case internal_user_id_to_connection_id(Owner, ServicePortId) of
+ {ok, ConnectionId} ->
+ call_service_port(ServicePortId, FunctionName, Arguments, ConnectionId, ExtraData);
+ {error, Reason} ->
+ {error, Reason}
+ end;
+
+call_service_port(ServicePortId, FunctionName, Arguments, ConnectionId, ExtraData) ->
+ ?LOGGING:log_call_to_bridge(ServicePortId, FunctionName, Arguments, ConnectionId, ExtraData),
+
?ROUTER:call_bridge(ServicePortId, #{ <<"type">> => <<"FUNCTION_CALL">>
- , <<"user_id">> => UserId
+ , <<"user_id">> => ConnectionId
, <<"value">> => #{ <<"function_name">> => FunctionName
, <<"arguments">> => Arguments
}
, <<"extra_data">> => ExtraData
}).
--spec get_how_to_enable(binary(), binary()) -> {ok, any()}.
-get_how_to_enable(ServicePortId, UserId) ->
+-spec get_how_to_enable(binary(), binary()) -> {ok, any()} | {error, atom()}.
+get_how_to_enable(ServicePortId, ConnectionId) ->
?ROUTER:call_bridge(ServicePortId, #{ <<"type">> => <<"GET_HOW_TO_SERVICE_REGISTRATION">>
- , <<"user_id">> => UserId
+ , <<"user_id">> => ConnectionId
, <<"value">> => #{}
}).
-spec send_registration_data(binary(), map(), binary()) -> {ok, map()}.
-send_registration_data(ServicePortId, Data, UserId) ->
+send_registration_data(ServicePortId, Data, ConnectionId) ->
?ROUTER:call_bridge(ServicePortId, #{ <<"type">> => <<"REGISTRATION">>
- , <<"user_id">> => UserId
+ , <<"user_id">> => ConnectionId
, <<"value">> => #{ <<"form">> => Data }
}).
@@ -75,22 +113,26 @@ send_oauth_return(Qs, ServicePortId) ->
, <<"value">> => #{ <<"query_string">> => Qs }
}).
--spec listen_bridge(binary(), binary()) -> ok | {error, term()}.
-listen_bridge(BridgeId, UserId) ->
- case ?BACKEND:get_or_create_monitor_id(UserId, BridgeId) of
- { ok, ChannelId } ->
- automate_channel_engine:listen_channel(ChannelId);
- {error, _X, Description} ->
- {error, Description}
+-spec listen_bridge(binary(), owner_id()) -> ok | {error, term()}.
+listen_bridge(BridgeId, Owner) when is_tuple(Owner) ->
+ listen_bridge(BridgeId, Owner, {undefined, undefined}).
+
+-spec listen_bridge(binary(), owner_id(), {binary()} | {binary() | undefined, binary() | undefined}) -> ok | {error, term()}.
+listen_bridge(BridgeId, Owner, Selector) when is_tuple(Owner) ->
+ case Selector of
+ {Key} ->
+ automate_service_port_engine_service:listen_service(Owner, {Key, undefined}, [BridgeId]);
+ {Key, SubKey} ->
+ automate_service_port_engine_service:listen_service(Owner, {Key, SubKey}, [BridgeId])
end.
--spec from_service_port(binary(), binary(), binary()) -> ok.
-from_service_port(ServicePortId, UserId, Msg) ->
- Unpacked = jiffy:decode(Msg, [return_maps]),
+-spec from_service_port(binary(), owner_id(), map()) -> ok.
+from_service_port(ServicePortId, Owner, Unpacked) when is_tuple(Owner) ->
automate_stats:log_observation(counter,
automate_bridge_engine_messages_from_bridge,
[ServicePortId]),
case Unpacked of
+ %% This has to be first because of the use of MessageId here
AdviceMsg = #{ <<"type">> := <<"ADVICE_SET">>, <<"message_id">> := MessageId } ->
AdviceTaken = apply_advice(AdviceMsg, ServicePortId),
answer_advice_taken(AdviceTaken, MessageId, self());
@@ -102,29 +144,63 @@ from_service_port(ServicePortId, UserId, Msg) ->
#{ <<"type">> := <<"CONFIGURATION">>
, <<"value">> := Configuration
} ->
- set_service_port_configuration(ServicePortId, Configuration, UserId);
-
- #{ <<"type">> := <<"NOTIFICATION">>
- , <<"key">> := Key
- , <<"to_user">> := ToUser
- , <<"value">> := Value
- , <<"content">> := Content
+ {ok, Todo} = set_service_port_configuration(ServicePortId, Configuration, Owner),
+ %% TODO: Check that it really exists, don't trust the DB
+ case lists:member(request_icon, Todo) of
+ false -> ok;
+ true ->
+ %% Request icon
+ request_icon(self())
+ end;
+
+ #{ <<"type">> := <<"ICON_UPLOAD">>
+ , <<"value">> := IconData
+ } ->
+ case IconData of
+ #{ <<"content">> := B64Content } ->
+ Data = base64:decode(B64Content),
+ ok = write_icon(Data, ServicePortId)
+ end;
+
+ #{ <<"type">> := <<"ESTABLISH_CONNECTION">>
+ , <<"value">> := #{ <<"connection_id">> := ConnectionId
+ , <<"name">> := Name
+ }
} ->
+ case ?BACKEND:establish_connection(ServicePortId, ConnectionId, Name) of
+ ok ->
+ io:fwrite("[~p] Established connection: ~p~n", [ServicePortId, ConnectionId]);
+ {error, Reason} ->
+ io:fwrite("[~p] Tried to establish connection but failed: ~p~n", [ServicePortId, Reason])
+ end;
+
+ Notif=#{ <<"type">> := <<"NOTIFICATION">>
+ , <<"key">> := Key
+ , <<"to_user">> := ToUser
+ , <<"value">> := Value
+ , <<"content">> := Content
+ } ->
case ToUser of
null ->
- %% TODO: This looping be removed if the users also listened on
- %% a common bridge channel. For this, the service API should allow
- %% returning multiple channels when asked.
- {ok, Channels} = ?BACKEND:list_bridge_channels(ServicePortId),
- Results = lists:map(fun (Channel) ->
- { Channel
+ {ok, Connections} = ?BACKEND:list_bridge_connections(ServicePortId),
+ Results = lists:map(fun (#user_to_bridge_connection_entry{ channel_id=ChannelId
+ , owner=ConnectionOwner
+ , save_signals=Save
+ }) ->
+ case Save of
+ true -> ?LOGGING:log_signal_to_bridge_and_owner(Notif, ServicePortId, ConnectionOwner);
+ false -> ok
+ end,
+ { ChannelId
, automate_channel_engine:send_to_channel(
- Channel,
+ ChannelId,
#{ <<"key">> => Key
, <<"value">> => Value
, <<"content">> => Content
+ , <<"subkey">> => get_subkey_from_notification(Notif)
+ , <<"service_id">> => ServicePortId
})}
- end, Channels),
+ end, Connections),
lists:foreach(
fun({Channel, Result}) ->
case Result of
@@ -136,99 +212,337 @@ from_service_port(ServicePortId, UserId, Msg) ->
%% messages had been sent
ok;
_ ->
- {ok, ToUserInternalId} = ?BACKEND:service_port_user_id_to_internal_user_id(
- ToUser, ServicePortId),
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(
- ServicePortId, ToUserInternalId),
-
- {ok, MonitorId } = automate_service_registry_query:get_monitor_id(
- Module, ToUserInternalId),
- ok = automate_channel_engine:send_to_channel(MonitorId,
- #{ <<"key">> => Key
- , <<"value">> => Value
- , <<"content">> => Content
- })
+ case ?BACKEND:get_connection_by_id(ToUser) of
+ {ok, #user_to_bridge_connection_entry{channel_id=ChannelId, bridge_id=ServicePortId, save_signals=Save}} ->
+ case Save of
+ true ->
+ ?LOGGING:log_signal_to_bridge_and_owner(Notif, ServicePortId, Owner);
+ false ->
+ ok
+ end,
+
+ case automate_channel_engine:send_to_channel(ChannelId,
+ #{ <<"key">> => Key
+ , <<"value">> => Value
+ , <<"content">> => Content
+ , <<"subkey">> => get_subkey_from_notification(Notif)
+ , <<"service_id">> => ServicePortId
+ }) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ automate_logging:log_platform(
+ error,
+ io_lib:format("[~p] Error propagating notification: ~p (conn: ~p, monitor_id: ~p)~n",
+ [ServicePortId, Reason, ToUser, ChannelId]))
+ end;
+ {error, Reason} ->
+ automate_logging:log_platform(
+ error,
+ io_lib:format("[~p] Error propagating notification (to ~p): ~p~n", [ServicePortId, ToUser, Reason]));
+ {ok, #user_to_bridge_connection_entry{bridge_id=OtherServicePortId}} ->
+ automate_logging:log_platform(
+ error,
+ io_lib:format("[~p] BridgeId ~p sent message to conenction with bridgeId ~p~n",
+ [?MODULE, ServicePortId, OtherServicePortId]))
+ end
end
end.
--spec list_custom_blocks(binary()) -> {ok, map()}.
-list_custom_blocks(UserId) ->
- ?BACKEND:list_custom_blocks(UserId).
+-spec list_custom_blocks(owner_id()) -> {ok, map()}.
+list_custom_blocks(Owner) when is_tuple(Owner) ->
+ ?BACKEND:list_custom_blocks(Owner).
--spec internal_user_id_to_service_port_user_id(binary(), binary()) -> {ok, binary()}.
-internal_user_id_to_service_port_user_id(UserId, ServicePortId) ->
- ?BACKEND:internal_user_id_to_service_port_user_id(UserId, ServicePortId).
+-spec internal_user_id_to_connection_id(owner_id(), binary()) -> {ok, binary()} | {error, not_found} | {error, any()}.
+internal_user_id_to_connection_id(Owner, ServicePortId) when is_tuple(Owner) ->
+ ?BACKEND:internal_user_id_to_connection_id(Owner, ServicePortId).
--spec get_user_service_ports(binary()) -> {ok, [#service_port_entry_extra{}]}.
-get_user_service_ports(UserId) ->
- {ok, Bridges} = ?BACKEND:get_user_service_ports(UserId),
+-spec get_user_service_ports(owner_id()) -> {ok, [#service_port_entry_extra{}]}.
+get_user_service_ports(Owner) when is_tuple(Owner) ->
+ {ok, Bridges} = ?BACKEND:get_user_service_ports(Owner),
{ok, lists:map(fun add_service_port_extra/1, Bridges)}.
--spec delete_bridge(binary(), binary()) -> ok | {error, binary()}.
-delete_bridge(UserId, BridgeId) ->
+-spec delete_bridge(owner_id(), binary()) -> ok | {error, binary()}.
+delete_bridge(Accessor, BridgeId) when is_tuple(Accessor) ->
ok = case ?BACKEND:get_service_id_for_port(BridgeId) of
{error, not_found} ->
ok;
{ok, ServiceId} ->
- automate_service_registry:delete_service(UserId, ServiceId)
+ automate_service_registry:delete_service(Accessor, ServiceId)
end,
- ?BACKEND:delete_bridge(UserId, BridgeId).
-
+ ?BACKEND:delete_bridge(Accessor, BridgeId).
+
+
+-spec callback_bridge(owner_id(), binary(), binary(), undefined | binary()) -> {ok, map() | [#{ id => binary(), name => binary() }]} | {error, term()}.
+callback_bridge(Owner, BridgeId, CallbackName, SequenceId) when is_tuple(Owner) ->
+ case internal_user_id_to_connection_id(Owner, BridgeId) of
+ {ok, ConnectionId} ->
+ case callback_bridge_through_connection(ConnectionId, BridgeId, CallbackName, SequenceId) of
+ {ok, #{ <<"result">> := Result } } ->
+ {ok, Result};
+ {error, Reason} ->
+ {error, Reason}
+ end;
+ {error, not_found} ->
+ case ?BACKEND:is_user_connected_to_bridge(Owner, BridgeId) of
+ {ok, false} ->
+ {error, not_found};
+ {ok, true, _Values} ->
+ %% No direct connection, but still connected (via shared connection)
+ %% We can pull the values from the share
+ %% TODO: Reformat values from the _Values already returned
+ {ok, Shares} = ?BACKEND:get_resources_shared_with(Owner),
+ Values = lists:filtermap(fun(#bridge_resource_share_entry{ connection_id=ConnectionId
+ , resource=Resource
+ , value=Value
+ , name=Name
+ }) ->
+ case Resource of
+ CallbackName ->
+ case get_connection_bridge(ConnectionId) of
+ {ok, BridgeId} ->
+ {true, #{ id => Value, name => Name}};
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end
+ end, Shares),
+ {ok, Values}
+ end;
+ {error, Reason} ->
+ {error, Reason}
+ end.
--spec callback_bridge(binary(), binary(), binary()) -> {ok, map()} | {error, term()}.
-callback_bridge(UserId, BridgeId, Callback) ->
- {ok, BridgeUserId} = internal_user_id_to_service_port_user_id(UserId, BridgeId),
+-spec callback_bridge_through_connection(binary(), binary(), binary(), undefined | binary()) -> {ok, map()} | {error, term()}.
+callback_bridge_through_connection(ConnectionId, BridgeId, CallbackName, SequenceId) ->
?ROUTER:call_bridge(BridgeId, #{ <<"type">> => <<"CALLBACK">>
- , <<"user_id">> => BridgeUserId
- , <<"value">> => #{ <<"callback">> => Callback
+ , <<"user_id">> => ConnectionId
+ , <<"value">> => #{ <<"callback">> => CallbackName
+ , <<"sequence_id">> => case SequenceId of
+ undefined -> null;
+ _ -> SequenceId
+ end
}
}).
-spec get_channel_origin_bridge(binary()) -> {ok, binary()} | {error, not_found}.
get_channel_origin_bridge(ChannelId) ->
- case automate_services_time:get_monitor_id(none) of
+ case automate_services_time:get_monitor_id() of
{ok, ChannelId} ->
{ok, automate_services_time:get_uuid()};
_ ->
?BACKEND:get_channel_origin_bridge(ChannelId)
end.
+-spec get_bridge_info(binary()) -> {ok, #service_port_metadata{}} | {error, not_found}.
+get_bridge_info(BridgeId) ->
+ ?BACKEND:get_bridge_info(BridgeId).
+
+-spec get_bridge_owner(binary()) -> {ok, owner_id()} | {error, not_found}.
+get_bridge_owner(BridgeId) ->
+ ?BACKEND:get_bridge_owner(BridgeId).
+
+-spec get_bridge_configuration(binary()) -> {ok, #service_port_configuration{}} | {error, not_found}.
+get_bridge_configuration(BridgeId) ->
+ ?BACKEND:get_bridge_configuration(BridgeId).
+
+-spec is_bridge_connected(binary()) -> {ok, boolean()} | {error, _}.
+is_bridge_connected(BridgeId) ->
+ ?ROUTER:is_bridge_connected(BridgeId).
+
+-spec list_established_connections(owner_id()) -> {ok, [#user_to_bridge_connection_entry{}]}.
+list_established_connections(Owner) when is_tuple(Owner) ->
+ ?BACKEND:list_established_connections(Owner).
+
+-spec list_established_connections(owner_id(), binary()) -> {ok, [#user_to_bridge_connection_entry{}]}.
+list_established_connections(Owner, BridgeId) when is_tuple(Owner) ->
+ ?BACKEND:list_established_connections(Owner, BridgeId).
+
+-spec get_connection_owner(binary()) -> {ok, owner_id()} | {error, not_found}.
+get_connection_owner(ConnectionId) ->
+ ?BACKEND:get_connection_owner(ConnectionId).
+
+-spec get_pending_connection_info(binary()) -> {ok, #user_to_bridge_pending_connection_entry{}}.
+get_pending_connection_info(ConnectionId) ->
+ ?BACKEND:get_pending_connection_info(ConnectionId).
+
+
+-spec is_module_connectable_bridge(owner_id(), module() | {module(), any()}) ->
+ false | {boolean(), {#service_port_entry{}, #service_port_configuration{}}}.
+is_module_connectable_bridge(Owner, {automate_service_port_engine_service, [ BridgeId | _ ]}) when is_tuple(Owner) ->
+ %% It *is* a bridge. Only remains to check if a new connection can be established.
+ case ?BACKEND:get_all_bridge_info(BridgeId) of
+ {error, _Reason} ->
+ false;
+ {ok, BridgeInfo, BridgeConfiguration} ->
+ IsConnectable = case BridgeConfiguration of
+ undefined -> false;
+ #service_port_configuration{ allow_multiple_connections=true } ->
+ true;
+ #service_port_configuration{ allow_multiple_connections=false } ->
+ case ?BACKEND:is_user_connected_to_bridge(Owner, BridgeId) of
+ {ok, true, _} ->
+ false;
+ {ok, false} ->
+ true
+ end
+ end,
+ {IsConnectable, {BridgeInfo, BridgeConfiguration}}
+ end;
+
+is_module_connectable_bridge(_, _) ->
+ %% Is not a bridge
+ false.
+
+
+-spec set_shared_resource(ConnectionId :: binary(), ResourceName :: binary(), Shares :: map()) -> ok.
+set_shared_resource(ConnectionId, ResourceName, Shares) ->
+ ?BACKEND:set_shared_resource(ConnectionId, ResourceName, Shares).
+
+-spec get_connection_shares(ConnectionId :: binary()) -> {ok, #{ binary() => #{ binary() => [ owner_id() ] } } }.
+get_connection_shares(ConnectionId) ->
+ ?BACKEND:get_connection_shares(ConnectionId).
+
+-spec get_connection_bridge(ConnectionId :: binary()) -> {ok, binary()} | {error, not_found}.
+get_connection_bridge(ConnectionId) ->
+ ?BACKEND:get_connection_bridge(ConnectionId).
+
+-spec get_resources_shared_with(Owner :: owner_id()) -> {ok, [#bridge_resource_share_entry{}]}.
+get_resources_shared_with(Owner) ->
+ ?BACKEND:get_resources_shared_with(Owner).
+
+-spec get_resources_shared_with_on_bridge(Owner :: owner_id(), BridgeId :: binary()) -> {ok, [#bridge_resource_share_entry{}]}.
+get_resources_shared_with_on_bridge(Owner, BridgeId) ->
+ {ok, Shares} = get_resources_shared_with(Owner),
+ {ok, lists:filter(fun(#bridge_resource_share_entry{ connection_id=ConnectionId }) ->
+ {ok, SharedBridgeId} = automate_service_port_engine:get_connection_bridge(ConnectionId),
+ SharedBridgeId == BridgeId
+ end, Shares)}.
+
+
+
+-spec create_bridge_token(BridgeId :: binary(), Owner :: owner_id(), TokenName :: binary(), ExpiresOn :: non_neg_integer() | undefined)
+ -> {ok, binary()} | {error, name_taken}.
+create_bridge_token(BridgeId, Owner, TokenName, ExpiresOn) ->
+ ?BACKEND:create_bridge_token(BridgeId, Owner, TokenName, ExpiresOn).
+
+-spec list_bridge_tokens(BridgeId :: binary()) -> {ok, [#bridge_token_entry{}]}.
+list_bridge_tokens(BridgeId) ->
+ ?BACKEND:list_bridge_tokens(BridgeId).
+
+-spec delete_bridge_token_by_name(BridgeId :: binary(), TokenName :: binary()) -> ok | {error, not_found}.
+delete_bridge_token_by_name(BridgeId, TokenName) ->
+ ?BACKEND:delete_bridge_token_by_name(BridgeId, TokenName).
+
+-spec check_bridge_token(BridgeId :: binary(), Token :: binary()) -> {ok, boolean()}.
+check_bridge_token(BridgeId, Token) ->
+ ?BACKEND:check_bridge_token(BridgeId, Token).
+
+-spec can_skip_authentication(BridgeId :: binary()) -> {ok, boolean()}.
+can_skip_authentication(BridgeId) ->
+ ?BACKEND:can_skip_authentication(BridgeId).
+
+-spec set_save_signals_on_connection(ConnectionId :: binary(), Owner :: owner_id(), SaveSignals :: boolean()) -> ok | {error, _}.
+set_save_signals_on_connection(ConnectionId, Owner, SaveSignals) ->
+ ?BACKEND:set_save_signals_on_connection(ConnectionId, Owner, SaveSignals).
+
%%====================================================================
%% Internal functions
%%====================================================================
--spec add_service_port_extra(#service_port_entry{}) -> #service_port_entry_extra{}.
-add_service_port_extra(#service_port_entry{ id=Id
- , name=Name
- , owner=Owner
- , service_id=ServiceId
- }) ->
- {ok, IsConnected} = ?ROUTER:is_bridge_connected(Id),
+-spec add_service_port_extra({#service_port_entry{}, #service_port_configuration{}}) -> #service_port_entry_extra{}.
+add_service_port_extra({#service_port_entry{ id=Id
+ , name=Name
+ , owner=Owner
+ }, Config}) ->
+ {ok, IsConnected} = is_bridge_connected(Id),
+
+ BridgeIcon = case Config of
+ undefined -> undefined;
+ #service_port_configuration{ icon=Icon } -> Icon
+ end,
#service_port_entry_extra{ id=Id
, name=Name
, owner=Owner
- , service_id=ServiceId
, is_connected=IsConnected
+ , icon=BridgeIcon
}.
-set_service_port_configuration(ServicePortId, Configuration, UserId) ->
- SPConfiguration = parse_configuration_map(ServicePortId, Configuration),
- ?BACKEND:set_service_port_configuration(ServicePortId, SPConfiguration, UserId),
- ok.
+set_service_port_configuration(ServicePortId, Configuration, Owner) ->
+ SPConfiguration = parse_configuration_map(ServicePortId, Configuration, Owner),
+ ?BACKEND:set_service_port_configuration(ServicePortId, SPConfiguration, Owner).
parse_configuration_map(ServicePortId,
- #{ <<"blocks">> := Blocks
- , <<"is_public">> := IsPublic
- , <<"service_name">> := ServiceName
- }) ->
+ Config=#{ <<"blocks">> := Blocks
+ , <<"is_public">> := RequestedPublic
+ , <<"service_name">> := ServiceName
+ }, Owner) ->
+ Resources = parse_resources(Config),
+
+ IsPublic = case RequestedPublic of
+ false ->
+ false;
+ true ->
+ case automate_storage:is_user_allowed_to_create_public_bridges(Owner) of
+ {ok, true} ->
+ true;
+ {ok, false} ->
+ automate_logging:log_platform(warning, list_to_binary(io_lib:format(
+ "[~p:~p] ~p tried to set a bridge to public (not allowed)",
+ [?MODULE, ?LINE, Owner]))),
+ false
+ end
+ end,
+
#service_port_configuration{ id=ServicePortId
- , is_public=IsPublic
- , service_id=undefined
- , service_name=ServiceName
- , blocks=lists:map(fun(B) -> parse_block(B) end, Blocks)
- }.
+ , is_public=IsPublic
+ , service_id=undefined
+ , service_name=ServiceName
+ , blocks=lists:map(fun(B) -> parse_block(B) end, Blocks)
+ , icon=get_icon_from_config(Config)
+ , allow_multiple_connections=get_allow_multiple_connections_from_config(Config)
+ , resources=lists:map(fun({Name, _Lockable}) -> Name end, Resources)
+ }.
+
+%% Find lockabel resources in configuration
+-spec parse_resources(map()) -> [{ Name :: binary(), Lockable :: boolean() }].
+parse_resources(#{ <<"resources">> := Resources }) ->
+ lists:map(fun(Resource=#{ <<"name">> := Name }) ->
+ case Resource of
+ #{ <<"properties">> := #{ <<"lockable">> := true }
+ } ->
+ {Name, true};
+ _ -> %% Not declared as lockable
+ {Name, false}
+ end
+ end, Resources);
+
+parse_resources(_) ->
+ [].
+
+
+-spec get_icon_from_config(map()) -> undefined | supported_icon_type().
+get_icon_from_config(#{ <<"icon">> := #{ <<"url">> := Url } }) ->
+ { url, Url };
+get_icon_from_config(#{ <<"icon">> := #{ <<"sha256">> := Hash } }) ->
+ Id={ hash, sha256, Hash },
+ %% TODO: Check that ID exists, or request to bridge
+ Id;
+get_icon_from_config(_) ->
+ undefined.
+
+-spec get_allow_multiple_connections_from_config(map()) -> boolean().
+get_allow_multiple_connections_from_config(#{ <<"allow_multiple_connections">> := AllowMultipleConnections })
+ when is_boolean(AllowMultipleConnections) ->
+ AllowMultipleConnections;
+get_allow_multiple_connections_from_config(_) ->
+ false.
+
+
parse_block(Block=#{ <<"arguments">> := Arguments
, <<"function_name">> := FunctionName
@@ -244,6 +558,7 @@ parse_block(Block=#{ <<"arguments">> := Arguments
, block_type=BlockType
, block_result_type=BlockResultType
, save_to=get_block_save_to(Block)
+ , show_in_toolbox=get_block_show_in_toolbox(Block)
};
parse_block(Block=#{ <<"arguments">> := Arguments
@@ -264,6 +579,7 @@ parse_block(Block=#{ <<"arguments">> := Arguments
, expected_value=ExpectedValue
, key=Key
, subkey=get_block_subkey(Block)
+ , show_in_toolbox=get_block_show_in_toolbox(Block)
}.
get_block_save_to(#{ <<"save_to">> := SaveTo }) ->
@@ -271,12 +587,27 @@ get_block_save_to(#{ <<"save_to">> := SaveTo }) ->
get_block_save_to(_) ->
undefined.
+get_block_show_in_toolbox(#{ <<"show_in_toolbox">> := ShowInToolbox }) when is_boolean(ShowInToolbox) ->
+ ShowInToolbox;
+get_block_show_in_toolbox(#{ <<"show_in_toolbox">> := ShowInToolbox
+ , <<"function_name">> := FunctionName
+ }) ->
+ automate_logging:log_bridge(warning, io_lib:format("'show_in_toolbox' parameter is not boolean (show_in_toolbox=~p) on function_name=~p", [ShowInToolbox, FunctionName])),
+ true; %% Default to true
+get_block_show_in_toolbox(_) ->
+ true.
+
get_block_subkey(#{ <<"subkey">> := SubKey }) ->
SubKey;
get_block_subkey(_) ->
undefined.
+get_subkey_from_notification(#{ <<"subkey">> := SubKey }) ->
+ SubKey;
+get_subkey_from_notification(_) ->
+ undefined.
+
parse_argument(#{ <<"default">> := DefaultValue
, <<"type">> := Type
@@ -286,20 +617,25 @@ parse_argument(#{ <<"default">> := DefaultValue
, class=undefined
};
-parse_argument(#{ <<"type">> := <<"variable">>
+parse_argument(Arg=#{ <<"type">> := <<"variable">>
, <<"class">> := Class
}) ->
- #service_port_block_static_argument{ type= <<"variable">>
+ #service_port_block_static_argument{ type=get_variable_type(Arg)
, class=Class
, default=undefined
};
-parse_argument(#{ <<"type">> := <<"variable">>
+parse_argument(Arg=#{ <<"type">> := <<"variable">>
}) ->
- #service_port_block_static_argument{ type= <<"variable">>
+ #service_port_block_static_argument{ type=get_variable_type(Arg)
, default=undefined
, class=undefined
};
+parse_argument(#{ <<"type">> := _Type
+ , <<"values">> := #{ <<"collection">> := Collection
+ }
+ }) ->
+ #service_port_block_collection_argument{ name=Collection };
parse_argument(#{ <<"type">> := Type
, <<"values">> := #{ <<"callback">> := Callback
@@ -307,11 +643,36 @@ parse_argument(#{ <<"type">> := Type
}) ->
#service_port_block_dynamic_argument{ callback=Callback
, type=Type
- }.
+ };
+
+parse_argument(#{ <<"type">> := Type
+ , <<"values">> := #{ <<"callback_sequence">> := CallbackSequence
+ }
+ }) ->
+ #service_port_block_dynamic_sequence_argument{ callback_sequence=CallbackSequence
+ , type=Type
+ }.
+
+get_variable_type(#{ <<"var_type">> := Type }) when Type =/= null ->
+ { <<"variable">>, Type };
+get_variable_type(_) ->
+ <<"variable">>.
answer_advice_taken(AdviceTaken, MessageId, Pid) ->
Pid ! {{ automate_service_port_engine, advice_taken}, MessageId, AdviceTaken }.
+request_icon(Pid) ->
+ Pid ! {{ automate_service_port_engine, request_icon} }.
+
+get_icon_path(ServicePortId) ->
+ binary:list_to_bin(
+ lists:flatten(io_lib:format("~s/~s", [automate_configuration:asset_directory("public/icons")
+ , ServicePortId
+ ]))).
+
+write_icon(Data, ServicePortId) ->
+ file:write_file(get_icon_path(ServicePortId), Data).
+
apply_advice(#{ <<"type">> := <<"ADVICE_SET">>
, <<"value">> := Value
}, BridgeId) ->
diff --git a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_configuration.erl b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_configuration.erl
index 6da0e8f7..ceb743ee 100644
--- a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_configuration.erl
+++ b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_configuration.erl
@@ -12,7 +12,7 @@
-include("../../automate_storage/src/versioning.hrl").
-spec get_versioning([node()]) -> #database_version_progression{}.
-get_versioning(_Nodes) ->
+get_versioning(Nodes) ->
%% Service port identity table
Version_1 = [ #database_version_data{ database_name=?SERVICE_PORT_TABLE
, records=[ id, name, owner, service_id ]
@@ -25,14 +25,14 @@ get_versioning(_Nodes) ->
, record_name=service_port_configuration
}
- %% Service port userId obfuscation
- , #database_version_data{ database_name=?SERVICE_PORT_USERID_OBFUSCATION_TABLE
+ %% Service port userId obfuscation (deprecated)
+ , #database_version_data{ database_name=automate_service_port_userid_obfuscation_table
, records=[ id, obfuscated_id ]
, record_name=service_port_user_obfuscation_entry
}
%% UserId×ServiceId -> ChannelId
- , #database_version_data{ database_name=?SERVICE_PORT_CHANNEL_TABLE
+ , #database_version_data{ database_name=automate_service_port_channel_table
, records=[ id, channel_id ]
, record_name=service_port_monitor_channel_entry
}
@@ -40,5 +40,437 @@ get_versioning(_Nodes) ->
#database_version_progression
{ base=Version_1
- , updates=[]
+ , updates=[ #database_version_transformation
+ %% 1. Add *User -> Bridge connection* table
+ %%
+ %% Keeps track of the bridges a user has authenticated himself into.
+ %%
+ %% 2. Delete the UserId-obfuscation table.
+ %%
+ %% This is now managed in the connections table.
+ %%
+ %% 3. Create a temporary "connection establishment" table
+ %%
+ %% This helps keep track of the ongoing registrations, for
+ %% processes that use side-channels, like chats.
+ { id=1
+ , apply=fun() ->
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_TO_BRIDGE_CONNECTION_TABLE
+ , records=[ id
+ , bridge_id
+ , user_id
+ , channel_id
+ , name
+ , creation_time
+ ]
+ , record_name=user_to_bridge_connection_entry
+ }, Nodes),
+
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE
+ , records=[ id
+ , bridge_id
+ , user_id
+ , channel_id
+ , creation_time
+ ]
+ , record_name=user_to_bridge_pending_connection_entry
+ }, Nodes),
+
+ ok = mnesia:wait_for_tables([ ?USER_TO_BRIDGE_CONNECTION_TABLE
+ , ?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE
+ ],
+ automate_configuration:get_table_wait_time()),
+
+ MigrateConnections =
+ fun() ->
+ Conversion =
+ fun({service_port_user_obfuscation_entry, {UserId, BridgeId}, ObfuscatedId}) ->
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
+ { user_to_bridge_connection_entry
+ , ObfuscatedId
+ , BridgeId
+ , UserId
+ , ChannelId
+ , undefined
+ , 0
+ }
+ end,
+ ok = db_map_table_to_table(automate_service_port_userid_obfuscation_table,
+ ?USER_TO_BRIDGE_CONNECTION_TABLE,
+ Conversion)
+ end,
+ {atomic, ok} = mnesia:transaction(MigrateConnections),
+
+ {atomic, ok} = mnesia:delete_table(automate_service_port_userid_obfuscation_table)
+ end
+ }
+
+ , #database_version_transformation
+ %% Add *icons* to service_configuration
+ { id=2
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?SERVICE_PORT_CONFIGURATION_TABLE,
+ fun({service_port_configuration, Id, ServiceName, ServiceId,
+ IsPublic, Blocks }) ->
+ %% Replicate the entry. Just set 'icon' to undefined.
+ {service_port_configuration, Id, ServiceName, ServiceId,
+ IsPublic, Blocks, undefined }
+ end,
+ [ id, service_name, service_id, is_public, blocks, icon ],
+ service_port_configuration
+ )
+ end
+ }
+
+ , #database_version_transformation
+ %% Add *allow_multiple_connection* to service_configuration table
+ { id=3
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?SERVICE_PORT_CONFIGURATION_TABLE,
+ fun({service_port_configuration, Id, ServiceName, ServiceId,
+ IsPublic, Blocks, Icon }) ->
+ %% Replicate the entry. Just set 'allow_multiple_connections' to false.
+ {service_port_configuration, Id, ServiceName, ServiceId,
+ IsPublic, Blocks, Icon, false }
+ end,
+ [ id, service_name, service_id, is_public, blocks, icon, allow_multiple_connections ],
+ service_port_configuration
+ )
+ end
+ }
+
+ , #database_version_transformation
+ %% Introduce user groups
+ { id=4
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?SERVICE_PORT_TABLE,
+ fun({service_port_entry
+ , Id, Name, Owner, ServiceId
+ }) ->
+ {service_port_entry
+ , Id, Name, {user, Owner}, ServiceId
+ }
+ end,
+ [ id, name, owner, service_id ],
+ service_port_entry
+ ),
+
+ ok = db_update_ids(automate_service_port_channel_table,
+ fun({ service_port_monitor_channel_entry
+ , {UserId, BridgeId}, ChannelId
+ }) ->
+ { service_port_monitor_channel_entry
+ , {{user, UserId}, BridgeId}, ChannelId
+ }
+ end),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_TO_BRIDGE_CONNECTION_TABLE,
+ fun({user_to_bridge_connection_entry
+ , Id, BridgeId, UserId, ChannelId, Name, CreationTime
+ }) ->
+ {user_to_bridge_connection_entry
+ , Id, BridgeId, {user, UserId}, ChannelId, Name, CreationTime
+ }
+ end,
+ [ id, bridge_id, owner, channel_id, name, creation_time ],
+ user_to_bridge_connection_entry
+ ),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE,
+ fun({user_to_bridge_pending_connection_entry
+ , Id, BridgeId, UserId, ChannelId, CreationTime
+ }) ->
+ {user_to_bridge_pending_connection_entry
+ , Id, BridgeId, {user, UserId}, ChannelId, CreationTime
+ }
+ end,
+ [ id, bridge_id, owner, channel_id, creation_time ],
+ user_to_bridge_pending_connection_entry
+ ),
+
+ ok = mnesia:wait_for_tables([ ?SERVICE_PORT_TABLE, ?USER_TO_BRIDGE_CONNECTION_TABLE
+ , ?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE
+ ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+
+ , #database_version_transformation
+ %% Introduce resources
+ { id=5
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?SERVICE_PORT_CONFIGURATION_TABLE,
+ fun({service_port_configuration, Id, ServiceName, ServiceId,
+ IsPublic, Blocks, Icon, AllowMultipleConnections }) ->
+ %% Replicate the entry. Just set 'resources' to empty list.
+ {service_port_configuration, Id, ServiceName, ServiceId,
+ IsPublic, Blocks, Icon, AllowMultipleConnections, [] }
+ end,
+ [ id, service_name, service_id, is_public, blocks, icon, allow_multiple_connections, resources ],
+ service_port_configuration
+ ),
+
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?SERVICE_PORT_SHARED_RESOURCES_TABLE
+ , records=[ connection_id
+ , resource
+ , value
+ , name
+ , shared_with
+ ]
+ , record_name=bridge_resource_share_entry
+ , type=bag
+ }, Nodes),
+
+ ok = mnesia:wait_for_tables([ ?SERVICE_PORT_SHARED_RESOURCES_TABLE
+ ], automate_configuration:get_table_wait_time()),
+
+ {atomic, ok} = mnesia:add_table_index(?SERVICE_PORT_SHARED_RESOURCES_TABLE, shared_with)
+ end
+ }
+
+ , #database_version_transformation
+ %% Introduce bridge tokens
+ { id=6
+ , apply=fun() ->
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?BRIDGE_TOKEN_TABLE
+ , records=[ token_key
+ , token_name
+ , bridge_id
+ , creation_time
+ , expiration_time
+ , last_connection_time
+ ]
+ , record_name=bridge_token_entry
+ , type=set
+ }, Nodes),
+
+ ok = mnesia:wait_for_tables([ ?BRIDGE_TOKEN_TABLE, ?SERVICE_PORT_TABLE
+ ], automate_configuration:get_table_wait_time()),
+
+ {atomic, ok} = mnesia:add_table_index(?BRIDGE_TOKEN_TABLE, bridge_id),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?SERVICE_PORT_TABLE,
+ fun({service_port_entry
+ , Id, Name, Owner, _ServiceId
+ }) ->
+ {service_port_entry
+ , Id, Name, Owner, true
+ }
+ end,
+ [ id, name, owner, old_skip_authentication ],
+ service_port_entry
+ )
+ end
+ }
+
+ , #database_version_transformation
+ %% Set log setting on bridge connections
+ { id=7
+ , apply=fun() ->
+
+ ok = mnesia:wait_for_tables([ ?USER_TO_BRIDGE_CONNECTION_TABLE
+ , automate_service_port_channel_table
+ ], automate_configuration:get_table_wait_time()),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_TO_BRIDGE_CONNECTION_TABLE,
+ fun(Entry) ->
+ case Entry of
+ { user_to_bridge_connection_entry
+ , Id, BridgeId, Owner, ChannelId, Name, CreationTime
+ } ->
+ {user_to_bridge_connection_entry
+ , Id, BridgeId, Owner, ChannelId, Name, CreationTime
+ , false
+ };
+ { user_to_bridge_connection_entry
+ , _Id, _BridgeId, _Owner, _ChannelId, _Name, _CreationTime
+ , _SaveSignals} ->
+ Entry
+ end
+ end,
+ [ id, bridge_id, owner, channel_id, name, creation_time, save_signals ],
+ user_to_bridge_connection_entry
+ ),
+
+ {atomic, ok} = mnesia:delete_table(automate_service_port_channel_table)
+ end
+ }
+
+ , #database_version_transformation
+ %% Index connection by owner_id
+ { id=8
+ , apply=fun() ->
+
+ ok = mnesia:wait_for_tables([ ?USER_TO_BRIDGE_CONNECTION_TABLE
+ ], automate_configuration:get_table_wait_time()),
+
+ {atomic, ok} = mnesia:add_table_index(?USER_TO_BRIDGE_CONNECTION_TABLE, owner)
+ end
+ }
+
+ , #database_version_transformation
+ %% Add `show_in_toolbox` to block descriptions
+ { id=9
+ , apply=fun() ->
+
+ ok = mnesia:wait_for_tables([ ?SERVICE_PORT_CONFIGURATION_TABLE
+ ], automate_configuration:get_table_wait_time()),
+
+ ok = automate_storage_configuration:db_map(
+ ?SERVICE_PORT_CONFIGURATION_TABLE
+ , fun({ service_port_configuration, Id, ServiceName, ServiceId, IsPublic
+ , Blocks
+ , Icon, AllowMultipleConnections, Resources
+ }) ->
+
+ NewBlocks = lists:map(fun(Block) ->
+ case Block of
+ { service_port_block
+ , BlockId
+ , FunctionName
+ , Message
+ , Arguments
+ , BlockType
+ , BlockResultType
+ , SaveTo
+ } ->
+ { service_port_block
+ , BlockId
+ , FunctionName
+ , Message
+ , Arguments
+ , BlockType
+ , BlockResultType
+ , SaveTo
+ , true %% show_in_toolbox
+ };
+ { service_port_trigger_block
+ , BlockId
+ , FunctionName
+ , Message
+ , Arguments
+ , BlockType
+ , SaveTo
+ , ExpectedValue
+ , Key
+ , Subkey
+ } ->
+ { service_port_trigger_block
+ , BlockId
+ , FunctionName
+ , Message
+ , Arguments
+ , BlockType
+ , SaveTo
+ , ExpectedValue
+ , Key
+ , Subkey
+ , true %% show_in_toolbox
+ };
+ %% Old trigger blocks
+ { service_port_trigger_block
+ , BlockId
+ , FunctionName
+ , Message
+ , Arguments
+ , BlockType
+ , SaveTo
+ , ExpectedValue
+ , Key
+ } ->
+ { service_port_trigger_block
+ , BlockId
+ , FunctionName
+ , Message
+ , Arguments
+ , BlockType
+ , SaveTo
+ , ExpectedValue
+ , Key
+ , undefined
+ , true %% show_in_toolbox
+ }
+ end
+ end, Blocks),
+
+ { service_port_configuration, Id, ServiceName, ServiceId, IsPublic
+ , NewBlocks
+ , Icon, AllowMultipleConnections, Resources
+ }
+ end
+ )
+ end
+ }
+ ]
}.
+
+
+
+db_map_table_to_table(FromTable, ToTable, Function) ->
+ Transaction = fun() ->
+ ok = mnesia:write_lock_table(FromTable),
+ ok = db_map_iter_transfer(FromTable, ToTable, Function, mnesia:first(FromTable))
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ io:fwrite("[Storage/Migration] Error on migration: ~p~n", [Reason]),
+ {error, Reason}
+ end.
+
+db_map_iter_transfer(_FromTable, _ToTable, _Function, '$end_of_table') ->
+ ok;
+db_map_iter_transfer(FromTable, ToTable, Function, Key) ->
+ [Element] = mnesia:read(FromTable, Key),
+ NewElement = Function(Element),
+
+ ok = mnesia:write(ToTable, NewElement, write),
+ db_map_iter_transfer(FromTable, ToTable, Function, mnesia:next(FromTable, Key)).
+
+%% DB map function to allow the conversion of ID columns
+db_update_ids(Database, Function) ->
+ Transaction = fun() ->
+ ok = mnesia:write_lock_table(Database),
+ ok = db_update_ids_iter(Database, Function, mnesia:first(Database), [])
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ io:fwrite("[Storage/Migration] Error on ID migration: ~p~n", [Reason]),
+ {error, Reason}
+ end.
+
+db_update_ids_iter(Database, _Function, '$end_of_table', Ops) ->
+ lists:foreach(fun({OldKey, _NewElement}) ->
+ ok = mnesia:delete(Database, OldKey, write)
+ end, Ops),
+ lists:foreach(fun({_OldKey, NewElement}) ->
+ ok = mnesia:write(Database, NewElement, write)
+ end, Ops),
+ ok;
+db_update_ids_iter(Database, Function, Key, Ops) ->
+ ElementsInKey = mnesia:read(Database, Key),
+ NewOps = lists:map(fun(Element) ->
+ NewElement = Function(Element),
+ {Key, NewElement}
+ end, ElementsInKey),
+ %% Operations (deletions) must be done at the end, to avoid interfering with mnesia:next
+ db_update_ids_iter(Database, Function, mnesia:next(Database, Key), Ops ++ NewOps).
diff --git a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_mnesia_backend.erl b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_mnesia_backend.erl
index 56433373..92c9428a 100644
--- a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_mnesia_backend.erl
+++ b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_mnesia_backend.erl
@@ -12,21 +12,50 @@
, get_signal_listeners/2
, list_custom_blocks/1
- , internal_user_id_to_service_port_user_id/2
- , service_port_user_id_to_internal_user_id/2
+ , get_block_definition/2
+ , internal_user_id_to_connection_id/2
+ , is_user_connected_to_bridge/2
+ , connection_id_to_internal_user_id/2
, get_user_service_ports/1
, list_bridge_channels/1
+ , list_bridge_connections/1
+ , list_established_connections/1
+ , list_established_connections/2
+ , get_connection_owner/1
+ , get_connection_by_id/1
+ , get_pending_connection_info/1
+
+ , gen_pending_connection/2
+ , establish_connection/4
+ , establish_connection/3
, get_service_id_for_port/1
+ , get_bridge_info/1
+ , get_bridge_owner/1
+ , get_bridge_configuration/1
+ , get_all_bridge_info/1
, delete_bridge/2
- , get_or_create_monitor_id/2
, uninstall/0
, get_channel_origin_bridge/1
+
+ , set_shared_resource/3
+ , get_connection_shares/1
+ , get_resources_shared_with/1
+ , get_connection_bridge/1
+
+ , create_bridge_token/4
+ , list_bridge_tokens/1
+ , delete_bridge_token_by_name/2
+ , check_bridge_token/2
+ , can_skip_authentication/1
+ , set_save_signals_on_connection/3
+ , check_save_signals_in_connection/1
]).
-include("records.hrl").
-include("databases.hrl").
+-include("../../automate_storage/src/security_params.hrl").
%%====================================================================
%% API
@@ -57,17 +86,15 @@ start_link() ->
uninstall() ->
{atomic, ok} = mnesia:delete_table(?SERVICE_PORT_TABLE),
{atomic, ok} = mnesia:delete_table(?SERVICE_PORT_CONFIGURATION_TABLE),
- {atomic, ok} = mnesia:delete_table(?SERVICE_PORT_USERID_OBFUSCATION_TABLE),
- {atomic, ok} = mnesia:delete_table(?SERVICE_PORT_CHANNEL_TABLE),
ok.
-
--spec create_service_port(binary(), binary()) -> {ok, binary()} | {error, _, string()}.
-create_service_port(UserId, ServicePortName) ->
+-spec create_service_port(owner_id(), binary()) -> {ok, binary()} | {error, _, string()}.
+create_service_port(Owner, ServicePortName) ->
ServicePortId = generate_id(),
Entry = #service_port_entry{ id=ServicePortId
, name=ServicePortName
- , owner=UserId
+ , owner=Owner
+ , old_skip_authentication=false %% Only old bridges can skip authentication
},
Transaction = fun() ->
@@ -81,6 +108,106 @@ create_service_port(UserId, ServicePortName) ->
{error, Reason, mnesia:error_description(Reason)}
end.
+-spec gen_pending_connection(binary(), owner_id()) -> {ok, binary()} | {error, not_authorized}.
+gen_pending_connection(BridgeId, Owner) ->
+ ConnectionId = generate_id(),
+ CurrentTime = erlang:system_time(second),
+
+ Transaction = fun() ->
+ case can_owner_establish_connection_to_bridge(Owner, BridgeId) of
+ {ok, true} ->
+ {ok, ChannelId} = automate_channel_engine:create_channel(),
+ Entry = #user_to_bridge_pending_connection_entry{ id=ConnectionId
+ , bridge_id=BridgeId
+ , owner=Owner
+ , channel_id=ChannelId
+ , creation_time=CurrentTime
+ },
+ ok = mnesia:write(?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE, Entry, write),
+ {ok, ConnectionId};
+ {ok, false} ->
+ {error, not_authorized}
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ {error, Reason, mnesia:error_description(Reason)}
+ end.
+
+%% Establish connection confirming Bridge and User id
+-spec establish_connection(binary(), owner_id(), binary(), binary()) -> ok | {error, not_found}.
+establish_connection(BridgeId, Owner, ConnectionId, Name) ->
+ CurrentTime = erlang:system_time(second),
+ Transaction = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE, ConnectionId) of
+ [] ->
+ {error, not_found};
+ [ #user_to_bridge_pending_connection_entry{ bridge_id=BridgeId
+ , owner=Owner
+ , channel_id=ChannelId
+ } ] ->
+ ok = mnesia:delete(?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE, ConnectionId, write),
+
+ Entry = #user_to_bridge_connection_entry{ id=ConnectionId
+ , bridge_id=BridgeId
+ , owner=Owner
+ , channel_id=ChannelId
+ , name=Name
+ , creation_time=CurrentTime
+ , save_signals=false
+ },
+ ok = mnesia:write(?USER_TO_BRIDGE_CONNECTION_TABLE, Entry, write),
+ {ok, ChannelId}
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, {ok, ChannelId}} ->
+ ok = automate_channel_engine:send_to_channel(ChannelId, connection_established),
+ ok;
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ {error, Reason}
+ end.
+
+%% Establish connection confirming Bridge id and recovering user.
+-spec establish_connection(binary(), binary(), binary()) -> ok | {error, not_found}.
+establish_connection(BridgeId, ConnectionId, Name) ->
+ CurrentTime = erlang:system_time(second),
+ Transaction = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE, ConnectionId) of
+ [] ->
+ {error, not_found};
+ [ #user_to_bridge_pending_connection_entry{ bridge_id=BridgeId
+ , owner=Owner
+ , channel_id=ChannelId
+ } ] ->
+ ok = mnesia:delete(?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE, ConnectionId, write),
+
+ Entry = #user_to_bridge_connection_entry{ id=ConnectionId
+ , bridge_id=BridgeId
+ , owner=Owner
+ , channel_id=ChannelId
+ , name=Name
+ , creation_time=CurrentTime
+ , save_signals=false
+ },
+ ok = mnesia:write(?USER_TO_BRIDGE_CONNECTION_TABLE, Entry, write),
+ {ok, ChannelId}
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, {ok, ChannelId}} ->
+ ok = automate_channel_engine:send_to_channel(ChannelId, connection_established),
+ ok;
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ {error, Reason}
+ end.
+
get_service_id_for_port(ServicePortId) ->
Transaction = fun() ->
case mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, ServicePortId) of
@@ -97,6 +224,76 @@ get_service_id_for_port(ServicePortId) ->
{error, Reason, mnesia:error_description(Reason)}
end.
+
+-spec get_bridge_info(binary()) -> {ok, #service_port_metadata{}} | {error, not_found}.
+get_bridge_info(BridgeId) ->
+ case get_all_bridge_info(BridgeId) of
+ { ok, #service_port_entry{name=Name, owner=Owner} , undefined} ->
+ {ok, #service_port_metadata{ id=BridgeId
+ , name=Name
+ , owner=Owner
+ , icon=undefined
+ }};
+ { ok, #service_port_entry{name=Name, owner=Owner} , #service_port_configuration{ icon=Icon }} ->
+ { ok, #service_port_metadata{ id=BridgeId
+ , name=Name
+ , owner=Owner
+ , icon=Icon
+ } } ;
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+-spec get_bridge_owner(binary()) -> {ok, owner_id()} | {error, not_found}.
+get_bridge_owner(BridgeId) ->
+ T = fun() ->
+ case mnesia:read(?SERVICE_PORT_TABLE, BridgeId) of
+ [] ->
+ {error, not_found};
+ [#service_port_entry{ owner=Owner }] ->
+ {ok, Owner}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:activity(ets, T)).
+
+
+-spec get_bridge_configuration(binary()) -> {ok, #service_port_configuration{}} | {error, not_found}.
+get_bridge_configuration(BridgeId) ->
+ T = fun() ->
+ case mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, BridgeId) of
+ [] ->
+ {error, not_found};
+ [Entry] ->
+ {ok, Entry}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:activity(ets, T)).
+
+
+-spec get_all_bridge_info(binary()) -> {ok, #service_port_entry{}, undefined | #service_port_configuration{}} | {error, _}.
+get_all_bridge_info(BridgeId) ->
+ Transaction = fun() ->
+ case mnesia:read(?SERVICE_PORT_TABLE, BridgeId) of
+ [] ->
+ {error, not_found};
+ [Entry] ->
+ case mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, BridgeId) of
+ [] ->
+ { ok, Entry, undefined };
+ [Config] ->
+ { ok, Entry, Config }
+ end
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ {error, Reason}
+ end.
+
+
+-spec create_service_for_port(#service_port_configuration{}, owner_id()) -> {ok, binary()}.
create_service_for_port(Configuration, OwnerId) ->
case Configuration of
#service_port_configuration{ is_public=true } ->
@@ -116,31 +313,56 @@ as_module(#service_port_configuration{ id=Id
, module => {automate_service_port_engine_service, [Id]}
}.
-set_service_port_configuration(ServicePortId, Configuration, OwnerId) ->
+-spec set_service_port_configuration(binary(), #service_port_configuration{}, owner_id()) -> {ok, [ request_icon ]}.
+set_service_port_configuration(ServicePortId, Configuration=#service_port_configuration{ icon=NewIcon
+ , is_public=IsPublic
+ }, OwnerId) ->
io:fwrite("Setting configuration: ~p~n", [Configuration]),
- ServiceId = case get_service_id_for_port(ServicePortId) of
- {ok, FoundServiceId} ->
- ok = automate_service_registry:update_service_module(as_module(Configuration),
- FoundServiceId,
- OwnerId),
- FoundServiceId;
- {error, not_found} ->
- {ok, NewServiceId} = create_service_for_port(Configuration, OwnerId),
- NewServiceId
- end,
-
Transaction = fun() ->
- mnesia:write(?SERVICE_PORT_CONFIGURATION_TABLE
- , Configuration#service_port_configuration{ service_id=ServiceId }
- , write
- )
+ ServiceId = case get_service_id_for_port(ServicePortId) of
+ {ok, FoundServiceId} ->
+ ok = automate_service_registry:update_service_module(as_module(Configuration),
+ FoundServiceId,
+ OwnerId),
+ ok = automate_service_registry:update_visibility(FoundServiceId, IsPublic),
+ case IsPublic of
+ false -> %% In case the service is not not public, make sure the owner is allowed
+ ok = automate_service_registry:allow_user(FoundServiceId, OwnerId);
+ _ -> ok
+ end,
+ FoundServiceId;
+ {error, not_found} ->
+ {ok, NewServiceId} = create_service_for_port(Configuration, OwnerId),
+ NewServiceId
+ end,
+
+ Previous = mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, ServicePortId),
+ ok = mnesia:write(?SERVICE_PORT_CONFIGURATION_TABLE
+ , Configuration#service_port_configuration{ service_id=ServiceId }
+ , write
+ ),
+ Previous
end,
case mnesia:transaction(Transaction) of
- {atomic, Result} ->
- Result;
+ {atomic, Previous} ->
+ Todo = case {NewIcon, Previous} of
+ {{ hash, HashType, Hash }, [#service_port_configuration{icon={hash, HashType, Hash}}]} ->
+ %% If it's the same hash, nothing to do
+ [];
+ {{ hash, _HashType, _Hash }, _} ->
+ %% If new is hash, and it's not the same as the old. Request an update
+ [ request_icon ];
+ {_, _} ->
+ %% If neither new nor old are hash, nothing to do
+ []
+ end,
+ {ok, Todo};
{aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ automate_logging:log_platform(error,
+ io_lib:format("Error saving configuration for bridge id=~p: ~p~n",
+ [ServicePortId, Reason])),
+ {error, Reason}
end.
-spec set_notify_signal_listeners([string()], binary()) -> ok.
@@ -179,61 +401,129 @@ get_signal_listeners(_Content, BridgeId) ->
Listeners
end, Channels )}.
--spec list_custom_blocks(binary()) -> {ok, map()}.
-list_custom_blocks(UserId) ->
+-spec list_custom_blocks(owner_id()) -> {ok, map()}.
+list_custom_blocks(Owner) ->
Transaction = fun() ->
- Services = list_userid_ports(UserId) ++ list_public_ports(),
- {ok
+ Connections = mnesia:index_read(?USER_TO_BRIDGE_CONNECTION_TABLE, Owner, #user_to_bridge_connection_entry.owner),
+ OwnBridgeIds = sets:to_list(sets:from_list(lists:map(
+ fun (#user_to_bridge_connection_entry{ bridge_id=BridgeId }) ->
+ BridgeId
+ end, Connections))),
+
+ BridgeIds = OwnBridgeIds ++ list_shared_ports(Owner),
+
+ { ok
, maps:from_list(
lists:filter(fun (X) -> X =/= none end,
- lists:map(fun (PortId) ->
- list_blocks_for_port(PortId)
+ lists:map(fun (BridgeId) ->
+ case is_user_connected_to_bridge(Owner, BridgeId) of
+ {ok, false} ->
+ none;
+ {ok, true, SharedResources } ->
+ list_blocks_for_port(BridgeId, SharedResources);
+ {error, not_found} ->
+ automate_logging:log_platform(error, io_lib:format("[~p:~p] Connection not found for bridge: ~p", [?MODULE, ?LINE, BridgeId])),
+ none
+ end
end,
- Services)))}
+ BridgeIds)))}
end,
- case mnesia:transaction(Transaction) of
- {atomic, Result} ->
- Result;
- {aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ automate_storage:wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec get_block_definition(BridgeId :: binary(), FunctionId :: binary()) -> {ok, #service_port_block{}}.
+get_block_definition(BridgeId, FunctionId) ->
+ T = fun() ->
+ mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, BridgeId)
+ end,
+ case mnesia:activity(ets, T) of
+ [ #service_port_configuration{ blocks=Blocks } ] ->
+ case lists:filter(fun(Block) ->
+ case Block of
+ #service_port_block{ block_id=BlockFunId } ->
+ BlockFunId == FunctionId;
+ _ ->
+ false
+ end
+ end, Blocks) of
+ [] -> {error, not_found};
+ %% Note that the case for multiple matches is not handled!
+ [ Block ] ->
+ {ok, Block}
+ end;
+ [] ->
+ {error, not_found}
+ end.
+
+
+-spec internal_user_id_to_connection_id(owner_id(), binary()) -> {ok, binary()} | {error, not_found} | { error, any() }.
+internal_user_id_to_connection_id(Owner, ServicePortId) ->
+ case get_all_connections(Owner, ServicePortId) of
+ {ok, []} ->
+ {error, not_found};
+ {ok, [H | _]} ->
+ {ok, H}; %% Return first
+ {error, Reason} ->
+ {error, Reason}
end.
--spec internal_user_id_to_service_port_user_id(binary(), binary()) -> {ok, binary()}.
-internal_user_id_to_service_port_user_id(UserId, ServicePortId) ->
- FullId = {UserId, ServicePortId},
+-spec get_all_connections(owner_id(), binary()) -> {ok, [binary()]} | {error, _}.
+get_all_connections({OwnerType, OwnerId}, BridgeId) ->
+ MatchHead = #user_to_bridge_connection_entry{ id='$1'
+ , bridge_id='$2'
+ , owner={'$3', '$4'}
+ , channel_id='_'
+ , name='_'
+ , creation_time='_'
+ , save_signals='_'
+ },
+ Guards = [ { '==', '$2', BridgeId }
+ , { '==', '$3', OwnerType }
+ , { '==', '$4', OwnerId }
+ ],
+ ResultColum = '$1',
+ Matcher = [{MatchHead, Guards, [ResultColum]}],
+
Transaction = fun() ->
- case mnesia:read(?SERVICE_PORT_USERID_OBFUSCATION_TABLE, FullId) of
- [] ->
- NewId = generate_id(),
- ok = mnesia:write(?SERVICE_PORT_USERID_OBFUSCATION_TABLE,
- #service_port_user_obfuscation_entry{ id=FullId
- , obfuscated_id=NewId
- }, write),
- {ok, NewId};
- [#service_port_user_obfuscation_entry{ obfuscated_id=ObfuscatedId }] ->
- {ok, ObfuscatedId}
- end
+ {ok, mnesia:select(?USER_TO_BRIDGE_CONNECTION_TABLE, Matcher)}
end,
case mnesia:transaction(Transaction) of
{atomic, Result} ->
Result;
{aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ {error, Reason}
end.
--spec service_port_user_id_to_internal_user_id(binary(), binary()) -> {ok, binary()}.
-service_port_user_id_to_internal_user_id(ServicePortUserId, ServicePortId) ->
+-spec is_user_connected_to_bridge(owner_id(), binary()) -> {ok, false} | {ok, true, all | #{binary() => [binary()] } } | {error, not_found}.
+is_user_connected_to_bridge(Owner, BridgeId) ->
+ case get_all_connections(Owner, BridgeId) of
+ {ok, []} ->
+ with_shared_connection(Owner, BridgeId);
+ {ok, List} when is_list(List) ->
+ {ok, true, all};
+ {error, not_found} ->
+ {error, not_found};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+-spec connection_id_to_internal_user_id(binary(), binary()) -> {ok, owner_id()} | {error, not_found}.
+connection_id_to_internal_user_id(ConnectionId, ServicePortId) ->
Transaction = fun() ->
- MatchHead = #service_port_user_obfuscation_entry{ id={ '$1', '$2' }
- , obfuscated_id='$3'
- },
+ MatchHead = #user_to_bridge_connection_entry{ id='$1'
+ , bridge_id='$2'
+ , owner='$3'
+ , channel_id='_'
+ , name='_'
+ , creation_time='_'
+ , save_signals='_'
+ },
Guards = [ { '==', '$2', ServicePortId }
- , { '==', '$3', ServicePortUserId }
+ , { '==', '$1', ConnectionId }
],
- ResultColum = '$1',
+ ResultColum = '$3',
Matcher = [{MatchHead, Guards, [ResultColum]}],
- mnesia:select(?SERVICE_PORT_USERID_OBFUSCATION_TABLE, Matcher)
+ mnesia:select(?USER_TO_BRIDGE_CONNECTION_TABLE, Matcher)
end,
case mnesia:transaction(Transaction) of
{atomic, [Result]} ->
@@ -245,19 +535,29 @@ service_port_user_id_to_internal_user_id(ServicePortUserId, ServicePortId) ->
{error, Reason, mnesia:error_description(Reason)}
end.
--spec get_user_service_ports(binary()) -> {ok, [map()]}.
-get_user_service_ports(UserId) ->
+-spec get_user_service_ports(owner_id()) -> {ok, [{#service_port_entry{}, #service_port_configuration{}}]}.
+get_user_service_ports({OwnerType, OwnerName}) ->
Transaction = fun() ->
MatchHead = #service_port_entry{ id='_'
, name='_'
- , owner='$1'
- , service_id='_'
+ , owner={'$1', '$2'}
+ , old_skip_authentication='_'
},
- Guard = {'==', '$1', UserId},
+ Guards = [ {'==', '$1', OwnerType}
+ , {'==', '$2', OwnerName}
+ ],
ResultColumn = '$_',
- Matcher = [{MatchHead, [Guard], [ResultColumn]}],
+ Matcher = [{MatchHead, Guards, [ResultColumn]}],
- {ok, mnesia:select(?SERVICE_PORT_TABLE, Matcher)}
+
+
+ {ok, lists:map(fun(Entry=#service_port_entry{ id=Id }) ->
+ Configuration = case mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, Id) of
+ [] -> undefined;
+ [Config] -> Config
+ end,
+ {Entry, Configuration}
+ end, mnesia:select(?SERVICE_PORT_TABLE, Matcher)) }
end,
case mnesia:transaction(Transaction) of
{atomic, Result} ->
@@ -267,31 +567,130 @@ get_user_service_ports(UserId) ->
end.
-spec list_bridge_channels(binary()) -> {ok, [binary()]}.
-list_bridge_channels(ServicePortId) ->
+list_bridge_channels(BridgeId) ->
+ MatchHead = #user_to_bridge_connection_entry{ id='_'
+ , bridge_id='$1'
+ , owner='_'
+ , channel_id='$2'
+ , name='_'
+ , creation_time='_'
+ , save_signals='_'
+ },
+ Guards = [ { '==', '$1', BridgeId } ],
+ ResultColum = '$2',
+ Matcher = [{MatchHead, Guards, [ResultColum]}],
+
+ Transaction = fun() ->
+ {ok, mnesia:select(?USER_TO_BRIDGE_CONNECTION_TABLE, Matcher)}
+ end,
+ mnesia:activity(ets, Transaction).
+
+-spec list_bridge_connections(BridgeId :: binary()) -> {ok, [#user_to_bridge_connection_entry{}]} | {error, not_found}.
+list_bridge_connections(BridgeId) ->
+ MatchHead = #user_to_bridge_connection_entry{ id='_'
+ , bridge_id='$1'
+ , owner='_'
+ , channel_id='_'
+ , name='_'
+ , creation_time='_'
+ , save_signals='_'
+ },
+ Guards = [ { '==', '$1', BridgeId } ],
+ ResultColum = '$_',
+ Matcher = [{MatchHead, Guards, [ResultColum]}],
+
+ Transaction = fun() ->
+ {ok, mnesia:select(?USER_TO_BRIDGE_CONNECTION_TABLE, Matcher)}
+ end,
+ mnesia:activity(ets, Transaction).
+
+-spec list_established_connections(owner_id()) -> {ok, [#user_to_bridge_connection_entry{}]} | {error, not_found}.
+list_established_connections({OwnerType, OwnerId}) ->
+ MatchHead = #user_to_bridge_connection_entry{ id='_'
+ , bridge_id='_'
+ , owner={'$1', '$2'}
+ , channel_id='_'
+ , name='_'
+ , creation_time='_'
+ , save_signals='_'
+ },
+ Guards = [ { '==', '$1', OwnerType }
+ , { '==', '$2', OwnerId }
+ ],
+ ResultColum = '$_',
+ Matcher = [{MatchHead, Guards, [ResultColum]}],
+
Transaction = fun() ->
- MatchHead = #service_port_monitor_channel_entry{ id={'_', '$1'}
- , channel_id='$2'
- },
- Guard = {'==', '$1', ServicePortId},
- ResultColumn = '$2',
- Matcher = [{MatchHead, [Guard], [ResultColumn]}],
-
- {ok, mnesia:select(?SERVICE_PORT_CHANNEL_TABLE, Matcher)}
+ {ok, mnesia:select(?USER_TO_BRIDGE_CONNECTION_TABLE, Matcher)}
+ end,
+ mnesia:activity(ets, Transaction).
+
+-spec list_established_connections(owner_id(), binary()) -> {ok, [#user_to_bridge_connection_entry{}]} | {error, not_found}.
+list_established_connections(Owner, BridgeId) ->
+ case list_established_connections(Owner) of
+ {ok, Results} ->
+ {ok, lists:filter(fun(#user_to_bridge_connection_entry{ bridge_id=ConnBridgeId }) ->
+ ConnBridgeId == BridgeId
+ end, Results)};
+ X ->
+ X
+ end.
+
+
+-spec get_connection_owner(binary()) -> {ok, owner_id()} | {error, not_found}.
+get_connection_owner(ConnectionId) ->
+ T = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_CONNECTION_TABLE, ConnectionId) of
+ [#user_to_bridge_connection_entry{owner=Owner}] ->
+ {ok, Owner};
+ [] ->
+ {error, not_found}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:activity(ets, T)).
+
+-spec get_connection_by_id(binary()) -> {ok, #user_to_bridge_connection_entry{}} | {error, not_found}.
+get_connection_by_id(ConnectionId) ->
+ T = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_CONNECTION_TABLE, ConnectionId) of
+ [Connection] ->
+ {ok, Connection};
+ [] ->
+ {error, not_found}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:activity(ets, T)).
+
+-spec get_pending_connection_info(binary()) -> {ok, #user_to_bridge_pending_connection_entry{}} | {error, not_found}.
+get_pending_connection_info(ConnectionId) ->
+ Transaction = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_PENDING_CONNECTION_TABLE, ConnectionId) of
+ [] ->
+ {error, not_found};
+ [ Connection ] ->
+ {ok, Connection}
+ end
end,
case mnesia:transaction(Transaction) of
{atomic, Result} ->
Result;
{aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ {error, mnesia:error_description(Reason)}
end.
--spec delete_bridge(binary(), binary()) -> ok | {error, binary()}.
-delete_bridge(UserId, BridgeId) ->
+
+-spec delete_bridge(owner_id(), binary()) -> ok | {error, binary()}.
+delete_bridge(Accessor, BridgeId) ->
Transaction = fun() ->
- [#service_port_entry{owner=UserId}] = mnesia:read(?SERVICE_PORT_TABLE, BridgeId),
- ok = mnesia:delete(?SERVICE_PORT_TABLE, BridgeId, write),
- ok = mnesia:delete(?SERVICE_PORT_CONFIGURATION_TABLE, BridgeId, write)
- %% TODO: remove user obfuscation entries
+ [#service_port_entry{owner=Owner}] = mnesia:read(?SERVICE_PORT_TABLE, BridgeId),
+ case automate_storage:can_user_edit_as(Accessor, Owner) of
+ true ->
+ ok = mnesia:delete(?SERVICE_PORT_TABLE, BridgeId, write),
+ ok = mnesia:delete(?SERVICE_PORT_CONFIGURATION_TABLE, BridgeId, write);
+ %% TODO: remove connection entries
+ false ->
+ {error, not_authorized}
+ end
end,
case mnesia:transaction(Transaction) of
{atomic, Result} ->
@@ -300,94 +699,358 @@ delete_bridge(UserId, BridgeId) ->
{error, mnesia:error_description(Reason)}
end.
--spec get_or_create_monitor_id(binary(), binary()) -> {ok, binary()} | {error, term(), binary()}.
-get_or_create_monitor_id(UserId, ServicePortId) ->
- Id = {UserId, ServicePortId},
- case mnesia:dirty_read(?SERVICE_PORT_CHANNEL_TABLE, Id) of
- [#service_port_monitor_channel_entry{channel_id=ChannelId}] ->
- {ok, ChannelId};
- [] ->
- {ok, ChannelId} = automate_channel_engine:create_channel(),
- Transaction = fun() ->
- ok = mnesia:write(?SERVICE_PORT_CHANNEL_TABLE,
- #service_port_monitor_channel_entry{ id=Id
- , channel_id=ChannelId},
- write),
-
- ChannelMonitors = mnesia:read(?SERVICE_PORT_CHANNEL_MONITORS_TABLE, ServicePortId),
- {ok, ChannelId, ChannelMonitors}
- end,
- case mnesia:transaction(Transaction) of
- {atomic, {ok, ChannelId, ChannelMonitors}} ->
- lists:foreach(fun(#channel_monitor_table_entry{pid=Pid}) ->
- Pid ! {automate_service_port_engine, new_channel, {ServicePortId, ChannelId} }
- end,
- ChannelMonitors),
- {ok, ChannelId};
- {atomic, Result} ->
- Result;
- {aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
- end
- end.
-
-spec get_channel_origin_bridge(binary()) -> {ok, binary()} | {error, not_found}.
get_channel_origin_bridge(ChannelId) ->
+ MatchHead = #user_to_bridge_connection_entry{ id='_'
+ , bridge_id='$2'
+ , owner='_'
+ , channel_id='$1'
+ , name='_'
+ , creation_time='_'
+ , save_signals='_'
+ },
+ Guards = [ { '==', '$1', ChannelId } ],
+ ResultColum = '$2',
+ Matcher = [{MatchHead, Guards, [ResultColum]}],
+
Transaction = fun() ->
- MatchHead = #service_port_monitor_channel_entry{ id='$1'
- , channel_id='$2'
- },
- Guard = {'==', '$2', ChannelId},
- ResultColumn = '$1',
- Matcher = [{MatchHead, [Guard], [ResultColumn]}],
-
- mnesia:select(?SERVICE_PORT_CHANNEL_TABLE, Matcher)
+ {ok, mnesia:select(?USER_TO_BRIDGE_CONNECTION_TABLE, Matcher)}
end,
- case mnesia:transaction(Transaction) of
- {atomic, []} ->
- {error, not_found};
- {atomic, [{_UserId, BridgeId}]} ->
- {ok, BridgeId};
- {aborted, Reason} ->
- {error, mnesia:error_description(Reason)}
- end.
+ mnesia:activity(ets, Transaction).
+
+-spec set_shared_resource(ConnectionId :: binary(), ResourceName :: binary(), Shares :: map()) -> ok.
+set_shared_resource(ConnectionId, ResourceName, Shares) ->
+ T = fun() ->
+ Existing = mnesia:read(?SERVICE_PORT_SHARED_RESOURCES_TABLE, ConnectionId),
+ AboutResource = lists:filter(fun(#bridge_resource_share_entry{resource=Resource}) ->
+ Resource == ResourceName
+ end, Existing),
+ ok = lists:foreach(fun(R) ->
+ ok = mnesia:delete_object(?SERVICE_PORT_SHARED_RESOURCES_TABLE, R, write)
+ end, AboutResource),
+ ok = lists:foreach(fun({ValueId, #{ <<"name">> := ValueName, <<"shared_with">> := Allowed } }) ->
+ ok = lists:foreach(fun(#{ <<"type">> := OwnerType
+ , <<"id">> := OwnerId
+ }) ->
+ ok = mnesia:write( ?SERVICE_PORT_SHARED_RESOURCES_TABLE
+ , #bridge_resource_share_entry{ connection_id=ConnectionId
+ , resource=ResourceName
+ , value=ValueId
+ , name=ValueName
+ , shared_with={map_owner_type(OwnerType), OwnerId}
+ }
+ , write)
+ end, Allowed)
+ end, maps:to_list(Shares))
+ end,
+ automate_storage:wrap_transaction(mnesia:transaction(T)).
+
+-spec get_connection_shares(ConnectionId :: binary()) -> {ok, #{ binary() => #{ binary() => [ owner_id() ] } } }.
+get_connection_shares(ConnectionId) ->
+ T = fun() ->
+ mnesia:read(?SERVICE_PORT_SHARED_RESOURCES_TABLE, ConnectionId)
+ end,
+ Permissions = automate_storage:wrap_transaction(mnesia:transaction(T)),
+ {ok, shares_list_to_map(Permissions)}.
+
+
+-spec get_resources_shared_with(Owner :: owner_id()) -> {ok, [#bridge_resource_share_entry{}]}.
+get_resources_shared_with(Owner) ->
+ T = fun() ->
+ mnesia:index_read(?SERVICE_PORT_SHARED_RESOURCES_TABLE, Owner, shared_with)
+ end,
+ {ok, automate_storage:wrap_transaction(mnesia:activity(ets, T))}.
+
+
+-spec get_connection_bridge(ConnectionId :: binary()) -> {ok, binary()} | {error, not_found}.
+get_connection_bridge(ConnectionId) ->
+ T = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_CONNECTION_TABLE, ConnectionId) of
+ [#user_to_bridge_connection_entry{bridge_id=BridgeId}] ->
+ {ok, BridgeId};
+ [] ->
+ {error, not_found}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:activity(ets, T)).
+
+
+-spec create_bridge_token(BridgeId :: binary(), Owner :: owner_id(), TokenName :: binary(), ExpiresOn :: undefined | non_neg_integer())
+ -> {ok, binary()} | {error, name_taken}.
+create_bridge_token(BridgeId, _Owner, TokenName, ExpiresOn) ->
+ TokenKey = generate_key(),
+ CurrentTime = erlang:system_time(second),
+
+ T = fun() ->
+ %% Check that there are no tokens with the same name
+ BridgeTokens = mnesia:index_read(?BRIDGE_TOKEN_TABLE, BridgeId, bridge_id),
+ MatchingTokens = lists:filter(fun(#bridge_token_entry{ token_name=Name}) ->
+ TokenName == Name
+ end, BridgeTokens),
+ case MatchingTokens of
+ [] ->
+ ok = mnesia:write(?BRIDGE_TOKEN_TABLE
+ , #bridge_token_entry{ token_key=TokenKey
+ , token_name=TokenName
+ , bridge_id=BridgeId
+ , creation_time=CurrentTime
+ , expiration_time=ExpiresOn
+ , last_connection_time=undefined
+ }, write),
+ {ok, TokenKey};
+ [#bridge_token_entry{}] ->
+ {error, name_taken}
+ end
+
+ end,
+ automate_storage:wrap_transaction(mnesia:transaction(T)).
+
+-spec list_bridge_tokens(BridgeId :: binary()) -> {ok, [#bridge_token_entry{}]}.
+list_bridge_tokens(BridgeId) ->
+ T = fun() ->
+ {ok, mnesia:index_read(?BRIDGE_TOKEN_TABLE, BridgeId, bridge_id)}
+ end,
+ automate_storage:wrap_transaction(mnesia:ets(T)).
+
+-spec delete_bridge_token_by_name(BridgeId :: binary(), TokenName :: binary()) -> ok | {error, not_found}.
+delete_bridge_token_by_name(BridgeId, TokenName) ->
+ %% TODO: Remove connection from bridges with that token
+ T = fun() ->
+ BridgeTokens = mnesia:index_read(?BRIDGE_TOKEN_TABLE, BridgeId, bridge_id),
+ MatchingTokens = lists:filter(fun(#bridge_token_entry{ token_name=Name}) ->
+ TokenName == Name
+ end, BridgeTokens),
+ case MatchingTokens of
+ [] -> {error, not_found};
+ [#bridge_token_entry{ token_key=Key }] ->
+ ok = mnesia:delete(?BRIDGE_TOKEN_TABLE, Key, write)
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:transaction(T)).
+
+-spec check_bridge_token(BridgeId :: binary(), Token :: binary()) -> {ok, boolean()}.
+check_bridge_token(BridgeId, Token) ->
+ CurrentTime = erlang:system_time(second),
+
+ T = fun() ->
+ case mnesia:read(?BRIDGE_TOKEN_TABLE, Token) of
+ [] -> {ok, false};
+ [TokenRec=#bridge_token_entry{bridge_id=BridgeId}] ->
+ %% TODO: Check that it hasn't expired
+
+ %% If the bridge could skip auth before, it no longer
+ %% needs it after authenticating correctly.
+ case mnesia:read(?SERVICE_PORT_TABLE, BridgeId) of
+ [#service_port_entry{old_skip_authentication=false}] ->
+ ok; %% Nothing to do
+ [Rec=#service_port_entry{old_skip_authentication=true}] ->
+ ok = mnesia:write(?SERVICE_PORT_TABLE
+ , Rec#service_port_entry{old_skip_authentication=false}
+ , write)
+ end,
+
+ %% Update token last used time
+ ok = mnesia:write(?BRIDGE_TOKEN_TABLE, TokenRec#bridge_token_entry{ last_connection_time=CurrentTime }, write),
+
+ {ok, true}; %% Nothing to do
+ [#bridge_token_entry{bridge_id=_OtherBridgeId}] ->
+ {ok, false}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:transaction(T)).
+
+-spec can_skip_authentication(BridgeId :: binary()) -> {ok, boolean()}.
+can_skip_authentication(BridgeId) ->
+ T = fun() ->
+ case mnesia:read(?SERVICE_PORT_TABLE, BridgeId) of
+ [] -> {ok, false};
+ [#service_port_entry{old_skip_authentication=CanSkip}] ->
+ {ok, CanSkip}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:ets(T)).
+
+-spec set_save_signals_on_connection(ConnectionId :: binary(), Owner :: owner_id(), SaveSignals :: boolean()) -> ok | {error, _}.
+set_save_signals_on_connection(ConnectionId, Owner, SaveSignals) ->
+ T = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_CONNECTION_TABLE, ConnectionId) of
+ [Conn=#user_to_bridge_connection_entry{owner=Owner}] ->
+ mnesia:write(?USER_TO_BRIDGE_CONNECTION_TABLE
+ , Conn#user_to_bridge_connection_entry{save_signals=SaveSignals}
+ , write
+ )
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:transaction(T)).
+
+-spec check_save_signals_in_connection(ConnectionId :: binary()) -> {ok, false} | {ok, true, {binary(), owner_id()}} | {error, not_found}.
+check_save_signals_in_connection(ConnectionId) ->
+ T = fun() ->
+ case mnesia:read(?USER_TO_BRIDGE_CONNECTION_TABLE, ConnectionId) of
+ [#user_to_bridge_connection_entry{bridge_id=BridgeId, owner=Owner, save_signals=Save}] ->
+ case Save of
+ false ->
+ {ok, false};
+ true ->
+ {ok, true, {BridgeId, Owner}}
+ end;
+ [] ->
+ {error, not_found}
+ end
+ end,
+ automate_storage:wrap_transaction(mnesia:ets(T)).
%%====================================================================
%% Internal functions
%%====================================================================
-list_userid_ports(UserId) ->
- MatchHead = #service_port_entry{ id='$1'
- , name='_'
- , owner='$2'
- , service_id='_'
- },
- Guard = {'==', '$2', UserId},
- ResultColumn = '$1',
- Matcher = [{MatchHead, [Guard], [ResultColumn]}],
-
- mnesia:select(?SERVICE_PORT_TABLE, Matcher).
-
-list_public_ports() ->
- MatchHead = #service_port_configuration{ id='$1'
- , service_name='_'
- , service_id='_'
- , is_public='$2'
- , blocks='_'
- },
- Guard = {'==', '$2', true},
- ResultColumn = '$1',
- Matcher = [{MatchHead, [Guard], [ResultColumn]}],
-
- mnesia:select(?SERVICE_PORT_CONFIGURATION_TABLE, Matcher).
-
-list_blocks_for_port(PortId) ->
+list_shared_ports(Owner) ->
+ {ok, Shares} = get_resources_shared_with(Owner),
+ lists:filtermap(fun(#bridge_resource_share_entry{ connection_id=ConnectionId }) ->
+ case get_connection_bridge(ConnectionId) of
+ {ok, BridgeId} -> {true, BridgeId};
+ {error, not_found} ->
+ automate_logging:log_platform(error, io_lib:format("[~p:~p] Bridge not found for connection: ~p", [?MODULE, ?LINE, ConnectionId])),
+ false
+ end
+ end, Shares).
+
+with_shared_connection(Owner, BridgeId) ->
+ {ok, Shares} = get_resources_shared_with(Owner),
+ SharedResources = lists:foldl(fun(#bridge_resource_share_entry{ connection_id=ConnectionId
+ , resource=Resource
+ , value=Value
+ }, Acc) ->
+ case get_connection_bridge(ConnectionId) of
+ {ok, BridgeId} ->
+ case Acc of
+ #{ Resource := PrevValues } ->
+ Acc#{ Resource => sets:add_element(Value, PrevValues) };
+ _ ->
+ Acc#{ Resource => sets:from_list([Value]) }
+ end;
+ {ok, _} ->
+ Acc;
+ {error, not_found} ->
+ automate_logging:log_platform(error, io_lib:format("[~p:~p] Bridge not found for connection: ~p", [?MODULE, ?LINE, ConnectionId])),
+ Acc
+ end
+ end, #{}, Shares),
+ case maps:size(SharedResources) > 0 of
+ false ->
+ {ok, false};
+ true ->
+ {ok, true, maps:map(fun(_K, V) -> sets:to_list(V) end, SharedResources)}
+ end.
+
+list_blocks_for_port(PortId, SharedResources) ->
case mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, PortId) of
[] -> none;
[#service_port_configuration{ blocks=Blocks
, service_id=ServiceId
}] ->
- {ServiceId, Blocks}
+ SharedBlocks = case SharedResources of
+ all -> Blocks;
+ Shares when is_map(Shares) ->
+ lists:filter(fun(Block) ->
+ case get_block_resources(Block) of
+ %% Shared blocks not refering any resource are a strange case.
+ %% For now, avoid relying on them.
+ [] -> false;
+ Resources ->
+ lists:all(fun(BlockResource) ->
+ maps:is_key(BlockResource, SharedResources)
+ end, Resources)
+ end
+ end, Blocks)
+ end,
+ {ServiceId, SharedBlocks}
end.
generate_id() ->
binary:list_to_bin(uuid:to_string(uuid:uuid4())).
+
+generate_key() ->
+ base64:encode(crypto:strong_rand_bytes(?KEY_RANDOM_LENGTH)).
+
+-spec shares_list_to_map([#bridge_resource_share_entry{}]) -> #{ binary() => #{ binary() => [ owner_id() ] } }.
+shares_list_to_map(Permissions) ->
+ shares_list_to_map(Permissions, #{}).
+
+shares_list_to_map([], Acc) ->
+ maps:map(fun(_K, Values) ->
+ maps:map(fun(_K2, Shares) ->
+ sets:to_list(Shares)
+ end, Values)
+ end, Acc);
+shares_list_to_map( [ #bridge_resource_share_entry{ resource=Resource
+ , value=Value
+ , shared_with=Owner } | T ]
+ , Acc) ->
+ WithShare = case Acc of
+ #{ Resource := ResourceVal=#{ Value := Shares } } ->
+ Acc#{ Resource => ResourceVal#{ Value => sets:add_element(Owner, Shares) } };
+ #{ Resource := ResourceVal } ->
+ Acc#{ Resource => ResourceVal#{ Value => sets:from_list([Owner]) } };
+ _ ->
+ Acc#{ Resource => #{ Value => sets:from_list([Owner]) } }
+ end,
+ shares_list_to_map(T, WithShare).
+
+map_owner_type(Type) when is_binary(Type) ->
+ case Type of
+ <<"group">> ->
+ group;
+ <<"user">> ->
+ user
+ end;
+map_owner_type(Type) when is_atom(Type) ->
+ Type.
+
+
+get_block_resources(Block) ->
+ Arguments = get_block_arguments(Block),
+ Res = lists:filtermap(fun(Arg) ->
+ case Arg of
+ #service_port_block_collection_argument{ name=Resource } ->
+ {true, Resource};
+ _ ->
+ false
+ end
+ end, Arguments ),
+ sets:to_list(sets:from_list(Res)).
+
+get_block_arguments(#service_port_block{ arguments=Arguments }) ->
+ Arguments;
+get_block_arguments(#service_port_trigger_block{ arguments=Arguments }) ->
+ Arguments.
+
+
+-spec can_owner_establish_connection_to_bridge(OwnerId :: owner_id(), BridgeId :: binary()) -> {ok, boolean()} | {error, not_found}.
+can_owner_establish_connection_to_bridge(OwnerId, BridgeId) ->
+ %% It can use a bridge if
+ %% - It's public
+ %% - It's the owner
+ %% - Or it's owned by a group where this user collaborates with enough level as to use the bridge
+ Transaction = fun() ->
+ case mnesia:read(?SERVICE_PORT_CONFIGURATION_TABLE, BridgeId) of
+ [#service_port_configuration{is_public=IsPublic}] ->
+ case IsPublic of
+ true -> {ok, true};
+ false ->
+ [#service_port_entry{owner=BridgeOwner}] = mnesia:read(?SERVICE_PORT_TABLE, BridgeId),
+ case BridgeOwner of
+ OwnerId -> {ok, true};
+ _ ->
+ case BridgeOwner of
+ {user, _} -> %% Not a group
+ {ok, false};
+ {group, GroupId} ->
+ automate_storage:is_user_allowed_to_connect_to_bridges_in_group(OwnerId, GroupId)
+ end
+ end
+ end;
+ [] ->
+ {error, not_found}
+ end
+ end,
+ mnesia:ets(Transaction).
diff --git a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_router.erl b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_router.erl
index 391f712a..b7f767ca 100644
--- a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_router.erl
+++ b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_router.erl
@@ -3,6 +3,7 @@
%% API
-export([ start_link/0
, connect_bridge/1
+ , disconnect_bridge/1
, call_bridge/2
, is_bridge_connected/1
, answer_message/2
@@ -15,8 +16,7 @@
-define(MAX_WAIT_TIME_SECONDS, 100).
-endif.
-define(MAX_WAIT_TIME, ?MAX_WAIT_TIME_SECONDS * 1000).
--define(CONNECTED_BRIDGES_TABLE, automate_service_port_connected_bridges_table).
--define(ON_FLIGHT_MESSAGES_TABLE, automate_service_port_bridge_on_flight_messages_table).
+-include("databases.hrl").
-include("records.hrl").
-include("router_error_cases.hrl").
@@ -28,7 +28,7 @@
%% @doc
%% Connect a bridge to the router.
%%
-%% @spec connect_bridge(BridgeId) -> ok | {error, Error}
+%% @spec connect_bridge(BridgeId :: binary()) -> ok | {error, Error}
%% @end
%%--------------------------------------------------------------------
connect_bridge(BridgeId) ->
@@ -49,6 +49,31 @@ connect_bridge(BridgeId) ->
{error, Error}
end.
+%%--------------------------------------------------------------------
+%% @doc
+%% Disconnect a bridge to the router.
+%%
+%% @spec connect_bridge(BridgeId :: binary()) -> ok | {error, Error}
+%% @end
+%%--------------------------------------------------------------------
+disconnect_bridge(BridgeId) ->
+ Pid = self(),
+ Node = node(),
+ Transaction = fun() ->
+ ok = mnesia:delete_object(?CONNECTED_BRIDGES_TABLE,
+ #bridge_connection_entry{ id=BridgeId
+ , pid=Pid
+ , node=Node
+ },
+ write)
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Error} ->
+ {error, Error}
+ end.
+
%%--------------------------------------------------------------------
%% @doc
%% Send a call to a bridge and return the result.
@@ -248,6 +273,6 @@ wait_bridge_response() ->
io:fwrite("[~p] Unexpected message: ~p~n", [?MODULE, X]),
wait_bridge_response()
after ?MAX_WAIT_TIME ->
- io:fwrite("[~p] Wait failed after ~pms~n", [?MODULE, ?MAX_WAIT_TIME]),
+ io:fwrite("[~p:~p] Wait failed after ~pms~n", [?MODULE, ?LINE, ?MAX_WAIT_TIME]),
{error, no_response}
end.
diff --git a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_service.erl b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_service.erl
index a560ef68..c565635a 100644
--- a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_service.erl
+++ b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_service.erl
@@ -9,13 +9,14 @@
-export([ start_link/0
, is_enabled_for_user/2
, get_how_to_enable/2
- , get_monitor_id/2
+ , listen_service/3
, call/5
- , send_registration_data/3
+ , send_registration_data/4
]).
-define(BACKEND, automate_service_port_engine_mnesia_backend).
-include("../../automate_bot_engine/src/program_records.hrl").
+-include("./records.hrl").
%%====================================================================
%% Service API
@@ -25,41 +26,174 @@
start_link() ->
ignore.
-%% No monitor associated with this service
-get_monitor_id(UserId, [ServicePortId]) ->
- ?BACKEND:get_or_create_monitor_id(UserId, ServicePortId).
-
--spec call(binary(), any(), #program_thread{}, binary(), _) -> {ok, #program_thread{}, any()}.
-call(FunctionName, Values, Thread, UserId, [ServicePortId]) ->
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServicePortId,
- UserId),
- {ok, MonitorId } = automate_service_registry_query:get_monitor_id(Module, UserId),
- LastMonitorValue = case automate_bot_engine_variables:get_last_monitor_value(
- Thread, MonitorId) of
+-spec listen_service(owner_id(), {binary() | undefined, binary() | undefined}, [binary(), ...]) -> ok | {error, no_valid_connection}.
+listen_service(Owner, {Key, SubKey}, [ServicePortId]) ->
+ case get_connection(Owner, ServicePortId, [{Key, SubKey}]) of
+ {ok, ConnectionId} ->
+ {ok, #user_to_bridge_connection_entry{channel_id=ChannelId}} = ?BACKEND:get_connection_by_id(ConnectionId),
+ automate_channel_engine:listen_channel(ChannelId, {Key, SubKey});
+ {error, not_found} ->
+ {error, no_valid_connection}
+ end.
+
+-spec call(binary(), any(), #program_thread{}, owner_id(), _) -> {ok, #program_thread{}, any()} | {error, no_connection | {failed, _} | timeout | no_valid_connection | {error_getting_resource, _}}.
+call(FunctionId, Values, Thread=#program_thread{program_id=ProgramId}, Owner, [ServicePortId]) ->
+ LastMonitorValue = case automate_bot_engine_variables:get_last_bridge_value(Thread, ServicePortId) of
{ok, Value} -> Value;
{error, not_found} -> null
end,
- {ok, ObfuscatedUserId} = automate_service_port_engine:internal_user_id_to_service_port_user_id(UserId, ServicePortId),
- {ok, #{ <<"result">> := Result }} = automate_service_port_engine:call_service_port(
- ServicePortId,
- FunctionName,
- Values,
- ObfuscatedUserId,
- #{ <<"last_monitor_value">> => LastMonitorValue}),
- {ok, Thread, Result}.
+ ConnectionId = case automate_bot_engine_variables:get_thread_context(Thread) of
+ { ok, #{ bridge_connection := #{ ServicePortId := ContextConnectionId } } } ->
+ ContextConnectionId;
+ _ ->
+ {ok, BlockInfo} = ?BACKEND:get_block_definition(ServicePortId, FunctionId),
+ try get_block_resource(BlockInfo, Values) of
+ Resources ->
+ case get_connection(Owner, ServicePortId, Resources) of
+ {ok, AvailableConnection} ->
+ AvailableConnection;
+ {error, not_found} ->
+ {error, no_valid_connection}
+ end
+ catch ErrorNS:Error:StackTrace ->
+ automate_logging:log_platform(error, ErrorNS, Error, StackTrace),
+ {error, {error_getting_resource, {ErrorNS, Error, StackTrace}}}
+ end
+ end,
+
+ case automate_service_port_engine:call_service_port(
+ ServicePortId,
+ FunctionId,
+ Values,
+ ConnectionId,
+ #{ <<"last_monitor_value">> => LastMonitorValue}) of
+ {ok, #{ <<"result">> := Result }} ->
+ ok = automate_storage:mark_successful_call_to_bridge(ProgramId, ServicePortId),
+ {ok, Thread, Result};
+ {ok, #{ <<"success">> := false, <<"error">> := Reason }} ->
+ ok = automate_storage:mark_failed_call_to_bridge(ProgramId, ServicePortId),
+ {error, {failed, Reason}};
+ {ok, #{ <<"success">> := false }} ->
+ ok = automate_storage:mark_failed_call_to_bridge(ProgramId, ServicePortId),
+ {error, {failed, undefined}};
+ {error, no_response} ->
+ ok = automate_storage:mark_failed_call_to_bridge(ProgramId, ServicePortId),
+ {error, timeout};
+ {error, Reason} ->
+ ok = automate_storage:mark_failed_call_to_bridge(ProgramId, ServicePortId),
+ {error, Reason}
+ end.
%% Is enabled for all users
-is_enabled_for_user(_Username, _Params) ->
+is_enabled_for_user(_Owner, _Params) ->
{ok, true}.
%% No need to enable service
-get_how_to_enable(#{ user_id := UserId }, [ServicePortId]) ->
- {ok, ObfuscatedUserId} = automate_service_port_engine:internal_user_id_to_service_port_user_id(UserId, ServicePortId),
- {ok, #{ <<"result">> := Result }} = automate_service_port_engine:get_how_to_enable(ServicePortId, ObfuscatedUserId),
- {ok, Result}.
-
-send_registration_data(UserId, RegistrationData, [ServicePortId]) ->
- {ok, ObfuscatedUserId} = automate_service_port_engine:internal_user_id_to_service_port_user_id(UserId, ServicePortId),
- {ok, Result} = automate_service_port_engine:send_registration_data(ServicePortId, RegistrationData, ObfuscatedUserId),
- {ok, Result}.
+-spec get_how_to_enable(owner_id(), [binary()]) -> {ok, map()} | {error, not_found}.
+get_how_to_enable(Owner, [ServicePortId]) ->
+ {ok, TemporaryConnectionId} = ?BACKEND:gen_pending_connection(ServicePortId, Owner),
+ case automate_service_port_engine:get_how_to_enable(ServicePortId, TemporaryConnectionId) of
+ {error, Err} ->
+ {error, Err};
+ {ok, Response} ->
+ case Response of
+ #{ <<"result">> := null } ->
+ {ok, #{ <<"type">> => <<"direct">> } };
+ #{ <<"result">> := Result } ->
+ {ok, Result#{ <<"connection_id">> => TemporaryConnectionId }};
+ _ ->
+ {ok, #{ <<"type">> => <<"direct">> } }
+
+ end
+ end.
+
+-spec send_registration_data(owner_id(), any(), [binary()], map()) -> {ok, any()}.
+send_registration_data(Owner, RegistrationData, [ServicePortId], Properties) ->
+ ConnectionId = case Properties of
+ #{ <<"connection_id">> := ConnId } when is_binary(ConnId) -> ConnId;
+ _ ->
+ {ok, TemporaryConnectionId} = ?BACKEND:gen_pending_connection(ServicePortId, Owner),
+ TemporaryConnectionId
+ end,
+
+ {ok, Result} = automate_service_port_engine:send_registration_data(ServicePortId, RegistrationData, ConnectionId),
+ PassedResult = case Result of
+ #{ <<"success">> := true } ->
+ Name = get_name_from_result(Result),
+ ok = ?BACKEND:establish_connection(ServicePortId, Owner, ConnectionId, Name),
+ Result;
+
+ #{ <<"success">> := false, <<"error">> := <<"No registerer available">> } ->
+ %% For compatibility with programaker-bridge library before connections
+ %% where introduced.
+ Name = get_name_from_result(Result),
+ ok = ?BACKEND:establish_connection(ServicePortId, Owner, ConnectionId, Name),
+ Result#{ <<"success">> => true
+ , <<"error">> => null
+ };
+
+ _ ->
+ Result
+ end,
+ {ok, PassedResult}.
+
+get_name_from_result(#{ <<"data">> := #{ <<"name">> := Name } }) ->
+ Name;
+get_name_from_result(_) ->
+ undefined.
+
+
+%%====================================================================
+%% Internal
+%%====================================================================
+-spec get_connection(Owner :: owner_id(), ServicePortId :: binary(), [{ binary() | undefined, binary() | undefined }])
+ -> {ok, binary()} | {error, not_found}.
+get_connection(Owner, ServicePortId, Resources) ->
+ case automate_service_port_engine:internal_user_id_to_connection_id(Owner, ServicePortId) of
+ {ok, DefaultConnectionId} ->
+ {ok, DefaultConnectionId};
+ {error, not_found} ->
+ {ok, Shares} = automate_service_port_engine:get_resources_shared_with_on_bridge(Owner, ServicePortId),
+ %% TODO: For usign blocks that require multiple resources it'd be necessary to consider
+ %% all resources shared for each connection. Instead of each share entry separately.
+ MatchingConnections = lists:filter(fun(#bridge_resource_share_entry{ resource=_SharedResource
+ , value=SharedResourceValue
+ }) ->
+ lists:all(fun({ _Key, ResourceValue }) ->
+ ResourceValue == SharedResourceValue
+ end, Resources)
+ end, Shares),
+ case MatchingConnections of
+ [#bridge_resource_share_entry{ connection_id=SharedConnectionId } | _] ->
+ {ok, SharedConnectionId};
+ [] ->
+ {error, not_found}
+ end
+ end.
+
+-spec get_block_resource(BlockInfo :: #service_port_block{}, Values :: [ any() ])
+ -> [{ binary(), binary()}].
+get_block_resource(#service_port_block{ arguments=Args, save_to=SaveTo }, Values) ->
+ SaveToIndex = case SaveTo of
+ #{ <<"type">> := <<"argument">>
+ , <<"index">> := Idx
+ } ->
+ Idx;
+ _ ->
+ -1
+ end,
+ get_block_resource_aux(Args, Values, SaveToIndex, 0, []).
+
+get_block_resource_aux([], [], _, _, Acc) ->
+ Acc;
+get_block_resource_aux([ _ | TArg], Values, SaveToIndex, CurrentIndex, Acc) when SaveToIndex == CurrentIndex ->
+ get_block_resource_aux(TArg, Values, SaveToIndex, CurrentIndex + 1, Acc);
+get_block_resource_aux([ #service_port_block_collection_argument{ name=Name } | TArg ], [ Value | TValue ], SaveToIndex, CurrentIndex, Acc) ->
+ get_block_resource_aux(TArg, TValue, SaveToIndex, CurrentIndex + 1, [{Name, Value} | Acc]);
+get_block_resource_aux([ _ | TArg ], [ _ | TValue ], SaveToIndex, CurrentIndex, Acc) ->
+ get_block_resource_aux(TArg, TValue, SaveToIndex, CurrentIndex + 1, Acc);
+get_block_resource_aux(_, _, _, _, Acc)->
+ %% TArgs and TValues don't have the same length.
+ %% One of them has stopped, so return the collected result.
+ Acc.
diff --git a/backend/apps/automate_service_port_engine/src/automate_service_port_engine_stats.erl b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_stats.erl
new file mode 100644
index 00000000..d22684ca
--- /dev/null
+++ b/backend/apps/automate_service_port_engine/src/automate_service_port_engine_stats.erl
@@ -0,0 +1,72 @@
+-module(automate_service_port_engine_stats).
+
+-export([ get_bridge_metrics/0
+ ]).
+
+
+-include("./databases.hrl").
+-include("./records.hrl").
+
+-spec get_bridge_metrics() -> { ok
+ , non_neg_integer(), non_neg_integer()
+ , non_neg_integer(), non_neg_integer()
+ , non_neg_integer()
+ }.
+get_bridge_metrics() ->
+ Transaction = fun() ->
+ %% Bridges
+ PublicMatchHead = #service_port_configuration{ id='_'
+ , service_name='_'
+ , service_id='_'
+ , is_public='$1'
+ , blocks='_'
+ , icon='_'
+ , allow_multiple_connections='_'
+ , resources='_'
+ },
+ PublicMatcher = [{ PublicMatchHead
+ , [{ '==', '$1', true }]
+ , ['_']}],
+ PrivateMatcher = [{ PublicMatchHead
+ , [{ '==', '$1', false }]
+ , ['_']}],
+
+ NumBridgesPublic = select_length(?SERVICE_PORT_CONFIGURATION_TABLE, PublicMatcher),
+ NumBridgesPrivate = select_length(?SERVICE_PORT_CONFIGURATION_TABLE, PrivateMatcher),
+
+ %% Connections and messages
+ NumConnections = mnesia:table_info(?CONNECTED_BRIDGES_TABLE, size),
+ OnFlightMessages = mnesia:table_info(?ON_FLIGHT_MESSAGES_TABLE, size),
+
+ ConnectionMatchHead = #bridge_connection_entry{ id='$1'
+ , pid='_'
+ , node='_'
+ },
+ ConnectionMatcher = [{ ConnectionMatchHead
+ , []
+ , ['$1']}],
+
+ NumUniqueConnections = select_unique_length(?CONNECTED_BRIDGES_TABLE, ConnectionMatcher),
+ { ok
+ , NumBridgesPublic, NumBridgesPrivate
+ , NumConnections, NumUniqueConnections
+ , OnFlightMessages
+ }
+ end,
+ mnesia:async_dirty(Transaction).
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+select_length(Tab, Matcher) ->
+ case mnesia:select(Tab, Matcher) of
+ Records ->
+ length(Records)
+ end.
+
+select_unique_length(Tab, Matcher) ->
+ case mnesia:select(Tab, Matcher) of
+ Records ->
+ Unique = sets:from_list(Records),
+ sets:size(Unique)
+ end.
diff --git a/backend/apps/automate_service_port_engine/src/databases.hrl b/backend/apps/automate_service_port_engine/src/databases.hrl
index 069edb00..3d08c49f 100644
--- a/backend/apps/automate_service_port_engine/src/databases.hrl
+++ b/backend/apps/automate_service_port_engine/src/databases.hrl
@@ -1,5 +1,12 @@
-define(SERVICE_PORT_TABLE, automate_service_port_table).
-define(SERVICE_PORT_CONFIGURATION_TABLE, automate_service_port_configuration_table).
--define(SERVICE_PORT_USERID_OBFUSCATION_TABLE, automate_service_port_userid_obfuscation_table).
--define(SERVICE_PORT_CHANNEL_TABLE, automate_service_port_channel_table).
-define(SERVICE_PORT_CHANNEL_MONITORS_TABLE, automate_service_port_channel_monitors_table).
+
+-define(USER_TO_BRIDGE_CONNECTION_TABLE, automate_service_port_channel_user_to_bridge_connection_table).
+-define(USER_TO_BRIDGE_PENDING_CONNECTION_TABLE, automate_service_port_channel_user_to_bridge_pending_connection_table).
+-define(SERVICE_PORT_SHARED_RESOURCES_TABLE, automate_service_port_shared_resources_table).
+-define(BRIDGE_TOKEN_TABLE, automate_service_port_bridge_token_table).
+
+%% Connections to bridges
+-define(CONNECTED_BRIDGES_TABLE, automate_service_port_connected_bridges_table).
+-define(ON_FLIGHT_MESSAGES_TABLE, automate_service_port_bridge_on_flight_messages_table).
diff --git a/backend/apps/automate_service_port_engine/src/records.hrl b/backend/apps/automate_service_port_engine/src/records.hrl
index e12ac8ef..d0173e8e 100644
--- a/backend/apps/automate_service_port_engine/src/records.hrl
+++ b/backend/apps/automate_service_port_engine/src/records.hrl
@@ -2,19 +2,10 @@
-record(service_port_entry, { id :: binary() | ?MNESIA_SELECTOR
, name :: binary() | ?MNESIA_SELECTOR
- , owner :: binary() | ?MNESIA_SELECTOR %% User id
- , service_id :: binary() | 'undefined' | ?MNESIA_SELECTOR
-%%% Note: This service ID is unused and to be dropped.
-%%% Check the service_port_configuration record.
+ , owner :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
+ , old_skip_authentication :: boolean() | ?MNESIA_SELECTOR
}).
--record(service_port_entry_extra, { id :: binary()
- , name :: binary()
- , owner :: binary() %% User id
- , service_id :: binary() | 'undefined'
- % ↓ Extra data
- , is_connected :: boolean()
- }).
-type service_port_block_argument_type() :: binary(). %% <<"string">>
%% | <<"integer">>
@@ -22,7 +13,7 @@
%% | <<"boolean">>
%% .
--record(service_port_block_static_argument, { type :: service_port_block_argument_type()
+-record(service_port_block_static_argument, { type :: service_port_block_argument_type() | { binary(), service_port_block_argument_type() }
, default :: binary() | 'undefined'
, class :: binary() | 'undefined'
}).
@@ -31,8 +22,17 @@
, callback :: binary()
}).
+-record(service_port_block_dynamic_sequence_argument, { type :: service_port_block_argument_type()
+ , callback_sequence :: [binary()]
+ }).
+
+-record(service_port_block_collection_argument, { name :: binary()
+ }).
+
-type service_port_block_argument() :: #service_port_block_static_argument{}
- | #service_port_block_dynamic_argument{}.
+ | #service_port_block_dynamic_argument{}
+ | #service_port_block_dynamic_sequence_argument{}
+ | #service_port_block_collection_argument{} .
-type block_save_to() :: null | #{ binary() => any()}.
-type block_subkey() :: null | #{ binary() => any()}.
@@ -44,6 +44,7 @@
, block_type :: binary()
, block_result_type :: binary()
, save_to :: block_save_to()
+ , show_in_toolbox :: boolean()
}).
-type service_port_trigger_expected_value() :: null | #{ binary() => any()}.
@@ -57,32 +58,43 @@
, expected_value :: service_port_trigger_expected_value()
, key :: binary()
, subkey :: block_subkey()
+ , show_in_toolbox :: boolean()
}).
+-type supported_icon_hashes() :: 'sha256'.
+
+-type supported_icon_type() :: {url, binary()}
+ | {hash, supported_icon_hashes(), binary() }.
+
-record(service_port_configuration, { id :: binary() | ?MNESIA_SELECTOR %% Service port Id
, service_name :: binary() | ?MNESIA_SELECTOR
, service_id :: binary() | 'undefined' | ?MNESIA_SELECTOR
, is_public :: boolean() | ?MNESIA_SELECTOR
, blocks :: [#service_port_block{}] | ?MNESIA_SELECTOR
+ , icon :: undefined | supported_icon_type() | ?MNESIA_SELECTOR
+ , allow_multiple_connections :: boolean() | ?MNESIA_SELECTOR
+ , resources :: [binary()] | ?MNESIA_SELECTOR
}).
+-record(service_port_entry_extra, { id :: binary()
+ , name :: binary()
+ , owner :: owner_id()
+ % ↓ Extra data
+ , is_connected :: boolean()
+ , icon :: supported_icon_type()
+ }).
--record(service_port_user_obfuscation_entry, { id :: { binary() | ?MNESIA_SELECTOR %% internal id
- , binary() | ?MNESIA_SELECTOR %% bridge id
- }
- , obfuscated_id :: binary() | ?MNESIA_SELECTOR
- }).
--record(service_port_monitor_channel_entry, { id :: { binary() | ?MNESIA_SELECTOR %% user id
- , binary() | ?MNESIA_SELECTOR %% bridge id
- } | ?MNESIA_SELECTOR
- , channel_id :: binary() | ?MNESIA_SELECTOR
- }).
+-record(service_port_metadata, { id :: binary() | ?MNESIA_SELECTOR
+ , name :: binary() | ?MNESIA_SELECTOR
+ , owner :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
+ , icon :: undefined | supported_icon_type() | ?MNESIA_SELECTOR
+ }).
--record(bridge_connection_entry, { id :: binary() %% Bridge id
- , pid :: pid() %% Connection pid
- , node :: atom() %% node() %% Node where the connection pid lives
+-record(bridge_connection_entry, { id :: binary() | ?MNESIA_SELECTOR %% Bridge id
+ , pid :: pid() | ?MNESIA_SELECTOR %% Connection pid
+ , node :: atom() | ?MNESIA_SELECTOR %% node() %% Node where the connection pid lives
}).
-record(on_flight_message_entry, { message_id :: binary()
@@ -95,3 +107,34 @@
, pid :: pid() | ?MNESIA_SELECTOR
, node :: node() | ?MNESIA_SELECTOR
}).
+
+-record(user_to_bridge_connection_entry, { id :: binary() | ?MNESIA_SELECTOR
+ , bridge_id :: binary() | ?MNESIA_SELECTOR
+ , owner :: owner_id() | ?MNESIA_SELECTOR | ?OWNER_ID_MNESIA_SELECTOR
+ , channel_id :: binary() | ?MNESIA_SELECTOR
+ , name :: binary() | undefined | ?MNESIA_SELECTOR
+ , creation_time :: non_neg_integer() | ?MNESIA_SELECTOR
+ , save_signals :: boolean() | ?MNESIA_SELECTOR
+ }).
+
+-record(user_to_bridge_pending_connection_entry, { id :: binary() | ?MNESIA_SELECTOR
+ , bridge_id :: binary() | ?MNESIA_SELECTOR
+ , owner :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
+ , channel_id :: binary() | ?MNESIA_SELECTOR
+ , creation_time :: non_neg_integer() | ?MNESIA_SELECTOR
+ }).
+
+-record(bridge_resource_share_entry, { connection_id :: binary() | ?MNESIA_SELECTOR
+ , resource :: binary() | ?MNESIA_SELECTOR
+ , value :: binary() | ?MNESIA_SELECTOR
+ , name :: binary() | ?MNESIA_SELECTOR
+ , shared_with :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
+ }).
+
+-record(bridge_token_entry, { token_key :: binary() | ?MNESIA_SELECTOR
+ , token_name :: binary() | ?MNESIA_SELECTOR
+ , bridge_id :: binary() | ?MNESIA_SELECTOR
+ , creation_time :: non_neg_integer() | ?MNESIA_SELECTOR
+ , expiration_time :: non_neg_integer() | undefined | ?MNESIA_SELECTOR
+ , last_connection_time :: non_neg_integer() | undefined | ?MNESIA_SELECTOR
+ }).
diff --git a/backend/apps/automate_service_port_engine/test/automate_service_port_engine_establish_connection_tests.erl b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_establish_connection_tests.erl
new file mode 100644
index 00000000..124e1d24
--- /dev/null
+++ b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_establish_connection_tests.erl
@@ -0,0 +1,134 @@
+%%% @doc
+%%% Automate service port custom blocks management tests.
+%%% @end
+
+-module(automate_service_port_engine_establish_connection_tests).
+-include_lib("eunit/include/eunit.hrl").
+-include("../src/records.hrl").
+
+%% Test data
+-define(APPLICATION, automate_service_port_engine).
+-define(TEST_NODES, [node()]).
+-define(BACKEND, automate_service_port_engine_mnesia_backend).
+-define(TEST_ID_PREFIX, "automate_service_port_engine_tests").
+-define(RECEIVE_TIMEOUT, 100).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _Pid} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% ?BACKEND:uninstall(),
+ ok = application:stop(?APPLICATION),
+
+ ok.
+
+tests(_SetupResult) ->
+ %% Custom blocks
+ [ { "[Service Port - Establish connection] Establish connection with owner user"
+ , fun establish_connection_with_owner_user/0
+ }
+ , { "[Service Port - Establish connection] Establish connection with owner group"
+ , fun establish_connection_with_owner_group/0
+ }
+ ].
+
+
+%%====================================================================
+%% Connection tests
+%%====================================================================
+establish_connection_with_owner_user() ->
+ OwnerUserId = {user, <>},
+ ServicePortName = <>,
+ {ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => []
+ },
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ ok = establish_connection(ServicePortId, OwnerUserId),
+
+ {ok, [ _Connection ]} = ?APPLICATION:list_established_connections(OwnerUserId).
+
+establish_connection_with_owner_group() ->
+ OwnerUserId = {group, <>},
+ ServicePortName = <>,
+ {ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => []
+ },
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }) ,
+
+ ok = establish_connection(ServicePortId, OwnerUserId),
+
+ {ok, [ _Connection ]} = ?APPLICATION:list_established_connections(OwnerUserId).
+
+
+
+%%====================================================================
+%% Auxiliary functions
+%%====================================================================
+establish_connection(ServicePortId, Owner) ->
+ Orig = self(),
+ Server = spawn(fun() ->
+ ok = automate_service_port_engine:register_service_port(ServicePortId),
+ receive
+ {automate_service_port_engine_router, _Pid, {data, MessageId, #{ <<"type">> := <<"GET_HOW_TO_SERVICE_REGISTRATION">> }}} ->
+ Anwser = ?APPLICATION:from_service_port(ServicePortId, Owner,
+ #{ <<"message_id">> => MessageId
+ , <<"success">> => true
+ , <<"result">> => #{ <<"type">> => <<"message">>
+ , <<"value">> => #{ <<"form">> => []}}
+ }),
+ io:fwrite("\033[41;37;1m Answer: ~p \033[0m~n", [Anwser])
+ end,
+ receive {connect, ConnectionId} ->
+ ?APPLICATION:from_service_port(ServicePortId, Owner,
+ #{ <<"type">> => <<"ESTABLISH_CONNECTION">>
+ , <<"value">> => #{ <<"connection_id">> => ConnectionId
+ , <<"name">> => ?MODULE
+ }
+ })
+ end,
+ Orig ! done
+ end),
+
+ {ok, #{module := Module}} = automate_service_registry:get_service_by_id(ServicePortId),
+ {ok, HowTo } = automate_service_registry_query:get_how_to_enable(Module, Owner),
+
+ case HowTo of
+ #{ <<"type">> := <<"message">>, <<"connection_id">> := ConnectionId } ->
+ Server ! {connect, ConnectionId},
+ receive done -> ok end
+ end.
diff --git a/backend/apps/automate_service_port_engine/test/automate_service_port_engine_router_tests.erl b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_router_tests.erl
index e9d5797a..56eb16b7 100644
--- a/backend/apps/automate_service_port_engine/test/automate_service_port_engine_router_tests.erl
+++ b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_router_tests.erl
@@ -150,4 +150,3 @@ route_two_to_zero() ->
Message = #{ value => sample },
{error, no_connection} = ?ROUTER:call_bridge(BridgeId, Message),
{error, no_connection} = ?ROUTER:call_bridge(BridgeId, Message).
-
diff --git a/backend/apps/automate_service_port_engine/test/automate_service_port_engine_shared_resource_tests.erl b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_shared_resource_tests.erl
new file mode 100644
index 00000000..93d65006
--- /dev/null
+++ b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_shared_resource_tests.erl
@@ -0,0 +1,928 @@
+%%% @doc
+%%% Automate service port shared resource management tests.
+%%% @end
+
+-module(automate_service_port_engine_shared_resource_tests).
+-include_lib("eunit/include/eunit.hrl").
+-include("../src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
+-include("../../automate_bot_engine/src/instructions.hrl").
+-include("../../automate_bot_engine/src/program_records.hrl").
+
+
+%% Test data
+-define(TEST_NODES, [node()]).
+-define(TEST_ID_PREFIX, "automate_service_port_engine_shared_resource_tests").
+-define(RECEIVE_TIMEOUT, 100).
+
+-define(APPLICATION, automate_service_port_engine).
+-define(BACKEND, automate_service_port_engine_mnesia_backend).
+-define(UTILS, automate_service_port_engine_test_utils).
+-define(BOT_UTILS, automate_bot_engine_test_utils).
+-define(ROUTER, automate_service_port_engine_router).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, _Pid} = application:ensure_all_started(?APPLICATION),
+
+ {NodeName}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName}) ->
+ %% ?BACKEND:uninstall(),
+ ok = application:stop(?APPLICATION),
+
+ ok.
+
+tests(_SetupResult) ->
+ %% Custom blocks
+ [ { "[Bridge - Shared resources] Allow to share resources, blocks that require a shared resource do appear"
+ , fun blocks_with_shared_resources_appear/0
+ }
+ , { "[Bridge - Shared resources] Allow to share resources, blocks that require a shared resource do appear (multiple resources)"
+ , fun blocks_with_shared_resources_appear_multiple_resources/0
+ }
+ , { "[Bridge - Shared resources] Allow to share resources, blocks that require a non-shared resource don't appear"
+ , fun non_shared_resources_negate_custom_blocks/0
+ }
+ , { "[Bridge - Shared resources] Allow to share resources, blocks that require a non-shared resource don't appear (multiple resources)"
+ , fun non_shared_resources_negate_custom_blocks_multiple_resources/0
+ }
+ , { "[Bridge - Shared resources] Allow to share resources, blocks that don't require resources don't appear"
+ , fun shared_block_with_no_resources_dont_appear/0
+ }
+ %% Execution test
+ , { "[Bridge - Shared resources] Allow to make calls on shared resource values"
+ , fun allow_to_make_calls_on_shared_resource_values/0
+ }
+ , { "[Bridge - Shared resources] Don't to make calls on non-shared resource values"
+ , fun disallow_calls_on_non_shared_resource_values/0
+ }
+ %% TODO: For this to be supported consider the TO-DO on automate_service_port_engine_service:get_connection()
+ %% , { "[Bridge - Shared resources] Allow to make calls on shared resource values (multiple resources)"
+ %% , fun allow_to_make_calls_on_shared_resource_values_multiple_resources/0
+ %% }
+ , { "[Bridge - Shared resources] Don't to make calls on non-shared resource values (multiple resources)"
+ , fun disallow_calls_on_non_shared_resource_values_multiple_resources/0
+ }
+ %% Routing tests
+ , { "[Bridge - Shared resources] Allow to listen on shared resource values"
+ , fun allow_to_listen_on_shared_resource_values/0
+ }
+ , { "[Bridge - Shared resources] Don't allow to listen on non-shared resource values (no subkey)"
+ , fun disallow_to_listen_on_non_shared_resources/0
+ }
+ , { "[Bridge - Shared resources] Don't allow to listen on non-shared resource values (different subkey)"
+ , fun disallow_to_listen_on_shared_resource_different_subkey/0
+ }
+ , { "[Bridge - Shared resources] Listening in a subkey doesn't make program receive different subkey messages"
+ , fun listening_on_shared_does_not_receive_different_subkeys/0
+ }
+ , { "[Bridge - Shared resources] Listening in a subkey doesn't make program receive different null subkey messages"
+ , fun listening_on_shared_does_not_receive_null_subkeys/0
+ }
+ ].
+
+
+%%====================================================================
+%% Custom block tests
+%%====================================================================
+blocks_with_shared_resources_appear() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ {ok, #{ BridgeId := [CustomBlock] } } = automate_service_port_engine:list_custom_blocks({group, GroupId}),
+ check_test_block(CustomBlock).
+
+blocks_with_shared_resources_appear_multiple_resources() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName1 = <<"channels">>,
+ SharedValue1 = <<"shared-val-id1">>,
+ SharedValueName1 = <<"shared-val-name1">>,
+ ResourceName2 = <<"group">>,
+ SharedValue2 = <<"shared-val-id2">>,
+ SharedValueName2 = <<"shared-val-name2">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName1}, {resource, ResourceName2}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName1
+ , #{ SharedValue1 =>
+ #{ <<"name">> => SharedValueName1
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName2
+ , #{ SharedValue2 =>
+ #{ <<"name">> => SharedValueName2
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ {ok, #{ BridgeId := [CustomBlock] } } = automate_service_port_engine:list_custom_blocks({group, GroupId}),
+ check_test_block(CustomBlock).
+
+non_shared_resources_negate_custom_blocks() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceNameShared = <<"channels">>,
+ ResourceNameNotShared = <<"non-channels">>,
+
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceNameNotShared}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceNameShared
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ?assertMatch({ok, #{ BridgeId := [] } }, automate_service_port_engine:list_custom_blocks({group, GroupId})).
+
+non_shared_resources_negate_custom_blocks_multiple_resources() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceNameNotShared = <<"non-channels">>,
+
+ ResourceNameShared1 = <<"channels1">>,
+ SharedValue1 = <<"shared-val-id1">>,
+ SharedValueName1 = <<"shared-val-name1">>,
+ ResourceNameShared2 = <<"groups">>,
+ SharedValue2 = <<"share-val-id2">>,
+ SharedValueName2 = <<"shared-val-name2">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceNameShared1}, {resource, ResourceNameNotShared}])
+ , get_test_block([{resource, ResourceNameNotShared}, {resource, ResourceNameShared1}])
+ ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceNameShared1
+ , #{ SharedValue1 =>
+ #{ <<"name">> => SharedValueName1
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceNameShared2
+ , #{ SharedValue2 =>
+ #{ <<"name">> => SharedValueName2
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ?assertMatch({ok, #{ BridgeId := [] } }, automate_service_port_engine:list_custom_blocks({group, GroupId})).
+
+
+shared_block_with_no_resources_dont_appear() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceNameShared = <<"channels">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceNameShared
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ?assertMatch({ok, #{ BridgeId := [] } }, automate_service_port_engine:list_custom_blocks({group, GroupId})).
+
+
+allow_to_make_calls_on_shared_resource_values() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+
+ BridgePid = test_bridge(BridgeId),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ {ok, ProgramId} = ?BOT_UTILS:create_user_program({group, GroupId}),
+ Thread = #program_thread{ position = [1]
+ , program=[ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ID => BridgeId
+ , ?SERVICE_ACTION => get_function_id()
+ , ?SERVICE_CALL_VALUES => [#{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => SharedValue
+ }]
+ }
+ }
+ ]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ ?assertMatch({ran_this_tick, NewThreadState, _}, automate_bot_engine_operations:run_thread(Thread, {?SIGNAL_PROGRAM_TICK, none}, undefined)).
+
+disallow_calls_on_non_shared_resource_values() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ SharedValue = <<"shared-val-id">>,
+ NonSharedValue = <<"non-shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+
+ Bridge = test_bridge(BridgeId),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ {ok, ProgramId} = ?BOT_UTILS:create_user_program({group, GroupId}),
+ Thread = #program_thread{ position = [1]
+ , program=[ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ID => BridgeId
+ , ?SERVICE_ACTION => get_function_id()
+ , ?SERVICE_CALL_VALUES => [#{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => NonSharedValue
+ }]
+ }
+ }
+ ]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ Ret = automate_bot_engine_operations:run_thread(Thread, {?SIGNAL_PROGRAM_TICK, none}, undefined),
+ Bridge ! done,
+ ?assertMatch({stopped, _}, Ret).
+
+allow_to_make_calls_on_shared_resource_values_multiple_resources() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName1 = <<"channels">>,
+ ResourceName2 = <<"groups">>,
+ SharedValue1 = <<"shared-val-id1">>,
+ SharedValue2 = <<"shared-val-id2">>,
+ NonSharedValue = <<"non-shared-val-id">>,
+ SharedValueName1 = <<"shared-val-name">>,
+ SharedValueName2 = <<"shared-val-name">>,
+ ReturnMessage = #{ <<"result">> => <<"ok">>} ,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName1}, {resource, ResourceName2}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+
+ Bridge = test_bridge(BridgeId),
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName1
+ , #{ SharedValue1 =>
+ #{ <<"name">> => SharedValueName1
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName2
+ , #{ SharedValue2 =>
+ #{ <<"name">> => SharedValueName2
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ {ok, ProgramId} = ?BOT_UTILS:create_user_program({group, GroupId}),
+ Thread = #program_thread{ position = [1]
+ , program=[ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ID => BridgeId
+ , ?SERVICE_ACTION => get_function_id()
+ , ?SERVICE_CALL_VALUES => [ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => SharedValue1
+ }
+ , #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => SharedValue2
+ }
+ ]
+ }
+ }
+ ]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ ?assertMatch({ran_this_tick, NewThreadState, _}, automate_bot_engine_operations:run_thread(Thread, {?SIGNAL_PROGRAM_TICK, none}, undefined)).
+
+disallow_calls_on_non_shared_resource_values_multiple_resources() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName1 = <<"channels">>,
+ ResourceName2 = <<"groups">>,
+ SharedValue1 = <<"shared-val-id1">>,
+ SharedValue2 = <<"shared-val-id2">>,
+ NonSharedValue = <<"non-shared-val-id">>,
+ SharedValueName1 = <<"shared-val-name">>,
+ SharedValueName2 = <<"shared-val-name">>,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName1}, {resource, ResourceName2}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ Bridge = test_bridge(BridgeId),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName1
+ , #{ SharedValue1 =>
+ #{ <<"name">> => SharedValueName1
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName2
+ , #{ SharedValue2 =>
+ #{ <<"name">> => SharedValueName2
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+ {ok, ProgramId} = ?BOT_UTILS:create_user_program({group, GroupId}),
+
+
+ Thread1 = #program_thread{ position = [1]
+ , program=[ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ID => BridgeId
+ , ?SERVICE_ACTION => get_function_id()
+ , ?SERVICE_CALL_VALUES => [ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => SharedValue1
+ }
+ , #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => NonSharedValue
+ }
+ ]
+ }
+ }
+ ]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ Ret = automate_bot_engine_operations:run_thread(Thread1, {?SIGNAL_PROGRAM_TICK, none}, undefined),
+ ?assertMatch({stopped, _}, Ret),
+
+ Thread2 = #program_thread{ position = [1]
+ , program=[ #{ ?TYPE => ?COMMAND_CALL_SERVICE
+ , ?ARGUMENTS => #{ ?SERVICE_ID => BridgeId
+ , ?SERVICE_ACTION => get_function_id()
+ , ?SERVICE_CALL_VALUES => [ #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => NonSharedValue
+ }
+ , #{ ?TYPE => ?VARIABLE_CONSTANT
+ , ?VALUE => SharedValue2
+ }
+ ]
+ }
+ }
+ ]
+ , global_memory=#{}
+ , instruction_memory=#{}
+ , program_id=ProgramId
+ , thread_id=undefined
+ },
+
+ Ret = automate_bot_engine_operations:run_thread(Thread2, {?SIGNAL_PROGRAM_TICK, none}, undefined),
+ Bridge ! done,
+ ?assertMatch({stopped, _}, Ret).
+
+
+allow_to_listen_on_shared_resource_values() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+ ReturnMessage = #{ <<"result">> => <<"ok">>} ,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ Bridge = test_bridge(BridgeId),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ok = automate_service_registry_query:listen_service(BridgeId, {group, GroupId}, { ResourceName, SharedValue }),
+ ok = ?APPLICATION:from_service_port(BridgeId, {group, GroupId},
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => ResourceName
+ , <<"to_user">> => ConnectionId
+ , <<"value">> => test
+ , <<"content">> => test
+ , <<"subkey">> => SharedValue
+ }),
+ receive {channel_engine, _ChannelId, Msg} ->
+ ?assertMatch(#{ <<"subkey">> := SharedValue }, Msg)
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end.
+
+
+disallow_to_listen_on_non_shared_resources() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+ ReturnMessage = #{ <<"result">> => <<"ok">>} ,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ Bridge = test_bridge(BridgeId),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ?assertMatch({error, no_valid_connection}, automate_service_registry_query:listen_service(BridgeId, {group, GroupId}, { ResourceName, undefined })).
+
+disallow_to_listen_on_shared_resource_different_subkey() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ NonSharedValue = <<"non-shared-val-id">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+ ReturnMessage = #{ <<"result">> => <<"ok">>} ,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ Bridge = test_bridge(BridgeId),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ?assertMatch({error, no_valid_connection}, automate_service_registry_query:listen_service(BridgeId, {group, GroupId}, { ResourceName, NonSharedValue })).
+
+listening_on_shared_does_not_receive_different_subkeys() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ NonSharedValue = <<"non-shared-val-id">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+ ReturnMessage = #{ <<"result">> => <<"ok">>} ,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ Bridge = test_bridge(BridgeId),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ok = automate_service_registry_query:listen_service(BridgeId, {group, GroupId}, { ResourceName, SharedValue }),
+
+ ok = ?APPLICATION:from_service_port(BridgeId, {group, GroupId},
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => ResourceName
+ , <<"to_user">> => ConnectionId
+ , <<"value">> => <<"test">>
+ , <<"content">> => <<"test">>
+ , <<"subkey">> => NonSharedValue
+ }),
+
+ ok = ?APPLICATION:from_service_port(BridgeId, {group, GroupId},
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => ResourceName
+ , <<"to_user">> => ConnectionId
+ , <<"value">> => <<"test">>
+ , <<"content">> => <<"test">>
+ , <<"subkey">> => SharedValue
+ }),
+ receive {channel_engine, _, Msg1} ->
+ ?assertMatch(#{ <<"subkey">> := SharedValue }, Msg1)
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+ receive {channel_engine, _, Msg2} ->
+ ct:fail("Should only receive a message, received extra: " ++ lists:flatten(io_lib:format("~p", [Msg2])))
+ after 0 ->
+ ok
+ end.
+
+
+listening_on_shared_does_not_receive_null_subkeys() ->
+ OwnerUser = {user, <>},
+ ReaderUserId = <>,
+ GroupName = <>,
+ ResourceName = <<"channels">>,
+ SharedValue = <<"shared-val-id">>,
+ SharedValueName = <<"shared-val-name">>,
+ ReturnMessage = #{ <<"result">> => <<"ok">>} ,
+
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUser, false),
+ ok = automate_storage:add_collaborators({ group, GroupId }, [{ReaderUserId, editor}]),
+
+ BridgeName = <>,
+ {ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUser, BridgeName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => BridgeName
+ , <<"blocks">> => [ get_test_block([{resource, ResourceName}]) ]
+ },
+ ok = ?APPLICATION:from_service_port(BridgeId, OwnerUser,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, ConnectionId} = ?UTILS:establish_connection(BridgeId, OwnerUser),
+ Bridge = test_bridge(BridgeId),
+
+ ok = automate_service_port_engine:set_shared_resource(ConnectionId
+ , ResourceName
+ , #{ SharedValue =>
+ #{ <<"name">> => SharedValueName
+ , <<"shared_with">> => [ #{ <<"id">> => GroupId
+ , <<"type">> => <<"group">>
+ }
+ ] } }),
+
+ ok = automate_service_registry_query:listen_service(BridgeId, {group, GroupId}, { ResourceName, SharedValue }),
+
+ ok = ?APPLICATION:from_service_port(BridgeId, {group, GroupId},
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => ResourceName
+ , <<"to_user">> => ConnectionId
+ , <<"value">> => <<"test">>
+ , <<"content">> => <<"test">>
+ , <<"subkey">> => null
+ }),
+
+ ok = ?APPLICATION:from_service_port(BridgeId, {group, GroupId},
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => ResourceName
+ , <<"to_user">> => ConnectionId
+ , <<"value">> => <<"test">>
+ , <<"content">> => <<"test">>
+ , <<"subkey">> => SharedValue
+ }),
+ receive {channel_engine, _, Msg1} ->
+ ?assertMatch(#{ <<"subkey">> := SharedValue }, Msg1)
+ after ?RECEIVE_TIMEOUT ->
+ ct:fail(timeout)
+ end,
+ receive {channel_engine, _, Msg2} ->
+ ct:fail("Should only receive a message, received extra: " ++ lists:flatten(io_lib:format("~p", [Msg2])))
+ after 0 ->
+ ok
+ end.
+
+
+%%====================================================================
+%% Custom block tests - Internal functions
+%%====================================================================
+-define(FunctionName, <<"first_function_name">>).
+-define(FunctionId, <<"first_function_id">>).
+-define(FunctionMessage, <<"sample message">>).
+-define(BlockType, <<"str">>).
+-define(BlockResultType, null).
+-define(SaveTo, undefined).
+
+get_function_id() ->
+ ?FunctionId.
+
+build_arguments(Args) ->
+ lists:map(fun(Arg) ->
+ case Arg of
+ {resource, Name} ->
+ #{ <<"type">> => <<"string">>
+ , <<"values">> => #{ <<"collection">> => Name }
+ };
+ Num when is_number(Num) ->
+ #{ <<"type">> => <<"integer">>
+ , <<"default">> => integer_to_binary(Num)
+ }
+ end
+ end, Args).
+
+get_test_block(Arguments) ->
+ #{ <<"arguments">> => build_arguments(Arguments)
+ , <<"function_name">> => ?FunctionName
+ , <<"message">> => ?FunctionMessage
+ , <<"id">> => ?FunctionId
+ , <<"block_type">> => ?BlockType
+ , <<"block_result_type">> => ?BlockResultType
+ }.
+
+check_test_block(Block) ->
+ ?assertMatch(#service_port_block{ block_id=?FunctionId
+ , function_name=?FunctionName
+ , message=?FunctionMessage
+ , arguments=_
+ , block_type=?BlockType
+ , block_result_type=?BlockResultType
+ , save_to=?SaveTo
+ }, Block).
+-undef(Arguments).
+-undef(FunctionName).
+-undef(FunctionId).
+-undef(FunctionMessage).
+-undef(BlockType).
+-undef(BlockResultType).
+-undef(SaveTo).
+
+test_bridge(BridgeId) ->
+ Orig = self(),
+ BridgePid = spawn(fun() ->
+ ok = ?ROUTER:connect_bridge(BridgeId),
+ Orig ! ready,
+ receive
+ { automate_service_port_engine_router
+ , _ %% From
+ , { data, MessageId, RecvMessage }} ->
+ ok = ?ROUTER:answer_message(MessageId, #{ <<"result">> => RecvMessage });
+ done ->
+ ok
+ end
+ end),
+ receive ready -> ok end,
+ BridgePid.
diff --git a/backend/apps/automate_service_port_engine/test/automate_service_port_engine_test_utils.erl b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_test_utils.erl
new file mode 100644
index 00000000..8d0dc831
--- /dev/null
+++ b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_test_utils.erl
@@ -0,0 +1,30 @@
+-module(automate_service_port_engine_test_utils).
+
+-define(BACKEND, automate_service_port_engine_mnesia_backend).
+
+-export([ establish_connection/2
+ , create_random_user/0
+ , set_admin_user/1
+ ]).
+
+establish_connection(BridgeId, UserId) ->
+ case ?BACKEND:gen_pending_connection(BridgeId, UserId) of
+ {ok, ConnectionId} ->
+ ok = ?BACKEND:establish_connection(BridgeId, UserId, ConnectionId, <<"test connection">>),
+ {ok, ConnectionId};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+
+create_random_user() ->
+ Username = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+ Password = undefined,
+ Email = binary:list_to_bin(uuid:to_string(uuid:uuid4())),
+
+ {ok, UserId} = automate_storage:create_user(Username, Password, Email, ready),
+ {Username, {user, UserId}}.
+
+
+set_admin_user({user, UserId}) ->
+ automate_storage:promote_user_to_admin(UserId).
diff --git a/backend/apps/automate_service_port_engine/test/automate_service_port_engine_tests.erl b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_tests.erl
index 2f3aab9d..e8750832 100644
--- a/backend/apps/automate_service_port_engine/test/automate_service_port_engine_tests.erl
+++ b/backend/apps/automate_service_port_engine/test/automate_service_port_engine_tests.erl
@@ -5,6 +5,7 @@
-module(automate_service_port_engine_tests).
-include_lib("eunit/include/eunit.hrl").
-include("../src/records.hrl").
+-include("../../automate_storage/src/records.hrl").
%% Test data
-define(APPLICATION, automate_service_port_engine).
@@ -12,6 +13,7 @@
-define(BACKEND, automate_service_port_engine_mnesia_backend).
-define(TEST_ID_PREFIX, "automate_service_port_engine_custom_blocks_tests").
-define(RECEIVE_TIMEOUT, 100).
+-define(UTILS, automate_service_port_engine_test_utils).
%%====================================================================
%% Test API
@@ -38,8 +40,8 @@ setup() ->
%% @doc App infrastructure teardown.
%% @end
-stop({NodeName}) ->
- ?BACKEND:uninstall(),
+stop({_NodeName}) ->
+ %% ?BACKEND:uninstall(),
ok = application:stop(?APPLICATION),
ok.
@@ -55,12 +57,18 @@ tests(_SetupResult) ->
, { "[Service Port - Custom blocks] Owned public blocks appear"
, fun owned_public_blocks_appear/0
}
+ , { "[Service Port - Custom blocks] Non-admin cannot set public bridge (and private blocks don't appear)"
+ , fun non_admin_user_cannot_set_public_bridge/0
+ }
, { "[Service Port - Custom blocks] Non-owned public blocks appear"
, fun non_owned_public_blocks_appear/0
}
, { "[Service Port - Custom blocks] Deleting a bridge deletes its custom blocks"
, fun owned_delete_bridge_blocks/0
}
+ , { "[Service Port - Custom blocks] Connect to private block from group"
+ , fun non_owned_public_blocks_from_group/0
+ }
%% Notification routing
, { "[Service port - Notifications] Route notifications targeted to owner"
, fun route_notification_targeted_to_owner/0
@@ -71,9 +79,19 @@ tests(_SetupResult) ->
, { "[Service port - Notifications] Route notifications targeted to non-owner on public"
, fun route_notification_targeted_to_non_owner_on_public/0
}
+ , { "[Service port - Notifications] Route notifications targeted to non-owner on non-public (because of non-admin)"
+ , fun route_notification_targeted_to_non_owner_on_shadowed_public/0
+ }
, { "[Service port - Notifications] Route notifications to all users on public"
, fun route_notification_targeted_to_all_users_on_public/0
}
+ %% Configuration
+ , { "[Service port - Configuration - Owner] A public bridge can be later set to private"
+ , fun set_public_bridge_to_private_owner/0
+ }
+ , { "[Service port - Configuration - Owner] A private bridge can be later set to public"
+ , fun set_private_bridge_to_public_owner/0
+ }
].
@@ -81,8 +99,9 @@ tests(_SetupResult) ->
%% Custom block tests
%%====================================================================
owned_private_blocks_appear() ->
- OwnerUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
ServicePortName = <>,
+
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
Configuration = #{ <<"is_public">> => false
@@ -90,17 +109,20 @@ owned_private_blocks_appear() ->
, <<"blocks">> => [ get_test_block() ]
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, OwnerUserId),
+
{ok, #{ ServicePortId := [CustomBlock] }} = ?APPLICATION:list_custom_blocks(OwnerUserId),
check_test_block(CustomBlock).
non_owned_private_blocks_dont_appear() ->
- OwnerUserId = <>,
- RequesterUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ {_, RequesterUserId} = ?UTILS:create_random_user(),
ServicePortName = <>,
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
@@ -109,15 +131,25 @@ non_owned_private_blocks_dont_appear() ->
, <<"blocks">> => [ get_test_block() ]
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ case ?BACKEND:gen_pending_connection(ServicePortId, RequesterUserId) of
+ {ok, ConnectionId} ->
+ %% TODO This connection should *not* be possible
+ ?BACKEND:establish_connection(ServicePortId, RequesterUserId, ConnectionId, undefined);
+ _ ->
+ ok
+ end,
+
{ok, Results} = ?APPLICATION:list_custom_blocks(RequesterUserId),
?assertEqual(#{}, Results).
owned_public_blocks_appear() ->
- OwnerUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ ok = ?UTILS:set_admin_user(OwnerUserId),
ServicePortName = <>,
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
@@ -126,17 +158,42 @@ owned_public_blocks_appear() ->
, <<"blocks">> => [ get_test_block() ]
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, OwnerUserId),
+
{ok, #{ ServicePortId := [CustomBlock] }} = ?APPLICATION:list_custom_blocks(OwnerUserId),
check_test_block(CustomBlock).
+non_admin_user_cannot_set_public_bridge() ->
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ {_, RequesterUserId} = ?UTILS:create_random_user(),
+ ServicePortName = <>,
+ {ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => true %% This should be ignored as the user is not an admin
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ get_test_block() ]
+ },
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ ?assertEqual({error, not_authorized}, ?UTILS:establish_connection(ServicePortId, RequesterUserId)),
+
+ {ok, Results} = ?APPLICATION:list_custom_blocks(RequesterUserId),
+ ?assertEqual(#{}, Results).
+
+
non_owned_public_blocks_appear() ->
- OwnerUserId = <>,
- RequesterUserId = <>,
- ServicePortName = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ ok = ?UTILS:set_admin_user(OwnerUserId),
+ {_, RequesterUserId} = ?UTILS:create_random_user(),
+ ServicePortName = <>,
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
Configuration = #{ <<"is_public">> => true
@@ -144,36 +201,41 @@ non_owned_public_blocks_appear() ->
, <<"blocks">> => [ get_test_block() ]
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, RequesterUserId),
+
{ok, #{ ServicePortId := [CustomBlock] }} = ?APPLICATION:list_custom_blocks(RequesterUserId),
check_test_block(CustomBlock).
owned_delete_bridge_blocks() ->
- OwnerUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
ServicePortName = <>,
{ok, BridgeId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
-
- Configuration = #{ <<"is_public">> => true
+ Configuration = #{ <<"is_public">> => false
, <<"service_name">> => ServicePortName
, <<"blocks">> => [ get_test_block() ]
},
ok = ?APPLICATION:from_service_port(BridgeId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, _} = ?UTILS:establish_connection(BridgeId, OwnerUserId),
+
{ok, #{ BridgeId := [CustomBlock] }} = ?APPLICATION:list_custom_blocks(OwnerUserId),
%% Blocks are created
check_test_block(CustomBlock),
{ok, ServiceId} = automate_service_port_engine_mnesia_backend:get_service_id_for_port(BridgeId),
- ?assertMatch({ok, _}, automate_service_registry:get_service_by_id(ServiceId, OwnerUserId)),
+ ?assertMatch({ok, _}, automate_service_registry:get_service_by_id(ServiceId)),
{ok, ResultsOk} = ?APPLICATION:list_custom_blocks(OwnerUserId),
?assertMatch({ok, _}, maps:find(ServiceId, ResultsOk)),
- {ok, BeforeDeleteServices} = automate_service_registry:get_all_services_for_user(OwnerUserId),
+ {ok, _BeforeDeleteServices} = automate_service_registry:get_all_services_for_user(OwnerUserId),
%% Delete bridge
ok = automate_service_port_engine:delete_bridge(OwnerUserId, BridgeId),
@@ -184,11 +246,38 @@ owned_delete_bridge_blocks() ->
%% Service deregistred
?assertEqual({error, not_found}, automate_service_port_engine_mnesia_backend:get_service_id_for_port(BridgeId)),
- ?assertEqual({error, not_found}, automate_service_registry:get_service_by_id(ServiceId, OwnerUserId)),
+ ?assertEqual({error, not_found}, automate_service_registry:get_service_by_id(ServiceId)),
{ok, Services} = automate_service_registry:get_all_services_for_user(OwnerUserId),
?assertEqual(error, maps:find(ServiceId, Services)).
+
+non_owned_public_blocks_from_group() ->
+ ServicePortName = <>,
+ GroupName = <>,
+
+ {_, OwnerId={user, OwnerUserId}} = ?UTILS:create_random_user(),
+ {ok, #user_group_entry{ id=GroupId }} = automate_storage:create_group(GroupName, OwnerUserId, false),
+ ok = automate_storage:update_group_metadata(GroupId, #{ min_level_for_private_bridge_usage => admin }),
+
+ {ok, ServicePortId} = ?APPLICATION:create_service_port({group, GroupId}, ServicePortName),
+
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ get_test_block() ]
+ },
+ ok = ?APPLICATION:from_service_port(ServicePortId, {group, GroupId},
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, OwnerId),
+
+ {ok, #{ ServicePortId := [CustomBlock] }} = ?APPLICATION:list_custom_blocks(OwnerId),
+
+ check_test_block(CustomBlock).
+
+
%%====================================================================
%% Custom block tests - Internal functions
%%====================================================================
@@ -230,7 +319,7 @@ check_test_block(Block) ->
%% Notification routing tests
%%====================================================================
route_notification_targeted_to_owner() ->
- OwnerUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
TargetUserId = OwnerUserId,
ServicePortName = <>,
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
@@ -242,22 +331,20 @@ route_notification_targeted_to_owner() ->
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
%% Listen on the service port
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServicePortId,
- TargetUserId),
- {ok, ChannelId } = automate_service_registry_query:get_monitor_id(Module, TargetUserId),
- ok = automate_channel_engine:listen_channel(ChannelId),
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, TargetUserId),
+ ok = automate_service_registry_query:listen_service(ServicePortId, TargetUserId, {undefined, undefined}),
%% Emit notification
{ok, ExpectedContent} = emit_notification(ServicePortId, OwnerUserId,
TargetUserId, #{ <<"test">> => 1 }),
%% Catch notification
- receive {channel_engine, ChannelId, ReceivedMessage} ->
+ receive {channel_engine, _ChannelId, ReceivedMessage} ->
?assertEqual(ExpectedContent, ReceivedMessage)
after ?RECEIVE_TIMEOUT ->
ct:fail(timeout)
@@ -265,7 +352,8 @@ route_notification_targeted_to_owner() ->
route_notification_targeted_to_owner_on_public() ->
- OwnerUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ ok = ?UTILS:set_admin_user(OwnerUserId),
TargetUserId = OwnerUserId,
ServicePortName = <>,
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
@@ -277,30 +365,29 @@ route_notification_targeted_to_owner_on_public() ->
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
%% Listen on the service port
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServicePortId,
- TargetUserId),
- {ok, ChannelId } = automate_service_registry_query:get_monitor_id(Module, TargetUserId),
- ok = automate_channel_engine:listen_channel(ChannelId),
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, TargetUserId),
+ ok = automate_service_registry_query:listen_service(ServicePortId, TargetUserId, {undefined, undefined}),
%% Emit notification
{ok, ExpectedContent} = emit_notification(ServicePortId, OwnerUserId,
TargetUserId, #{ <<"test">> => 2 }),
%% Catch notification
- receive {channel_engine, ChannelId, ReceivedMessage} ->
+ receive {channel_engine, _ChannelId, ReceivedMessage} ->
?assertEqual(ExpectedContent, ReceivedMessage)
after ?RECEIVE_TIMEOUT ->
ct:fail(timeout)
end.
route_notification_targeted_to_non_owner_on_public() ->
- OwnerUserId = <>,
- TargetUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ ok = ?UTILS:set_admin_user(OwnerUserId),
+ {_, TargetUserId} = ?UTILS:create_random_user(),
ServicePortName = <>,
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
@@ -311,30 +398,50 @@ route_notification_targeted_to_non_owner_on_public() ->
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
%% Listen on the service port
- {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServicePortId,
- TargetUserId),
- {ok, ChannelId } = automate_service_registry_query:get_monitor_id(Module, TargetUserId),
- ok = automate_channel_engine:listen_channel(ChannelId),
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, TargetUserId),
+ ok = automate_service_registry_query:listen_service(ServicePortId, TargetUserId, {undefined, undefined}),
%% Emit notification
{ok, ExpectedContent} = emit_notification(ServicePortId, OwnerUserId,
TargetUserId, #{ <<"test">> => 3 }),
%% Catch notification
- receive {channel_engine, ChannelId, ReceivedMessage} ->
+ receive {channel_engine, _ChannelId, ReceivedMessage} ->
?assertEqual(ExpectedContent, ReceivedMessage)
after ?RECEIVE_TIMEOUT ->
ct:fail(timeout)
end.
+route_notification_targeted_to_non_owner_on_shadowed_public() ->
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ %% User not set to admin
+ {_, TargetUserId} = ?UTILS:create_random_user(),
+ ServicePortName = <>,
+ {ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
+
+ %% Configure service port
+ Configuration = #{ <<"is_public">> => true %% This configuration is set to `is_public => false` as the owner is not admin
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ %% Listen on the service port
+ ?assertEqual({error, not_authorized}, ?UTILS:establish_connection(ServicePortId, TargetUserId)).
+
route_notification_targeted_to_all_users_on_public() ->
- OwnerUserId = <>,
- TargetUserId = <>,
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ ok = ?UTILS:set_admin_user(OwnerUserId),
+ {_, TargetUserId} = ?UTILS:create_random_user(),
ServicePortName = <>,
{ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
@@ -345,40 +452,110 @@ route_notification_targeted_to_all_users_on_public() ->
},
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"CONFIGURATION">>
- , <<"value">> => Configuration
- })),
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
%% Listen on the service port for non-owner
- {ok, #{ module := NonOwnerModule }} = automate_service_registry:get_service_by_id(
- ServicePortId, TargetUserId),
- {ok, NonOwnerChannelId } = automate_service_registry_query:get_monitor_id(
- NonOwnerModule, TargetUserId),
- ok = automate_channel_engine:listen_channel(NonOwnerChannelId),
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, TargetUserId),
+ ok = automate_service_registry_query:listen_service(ServicePortId, TargetUserId, {undefined, undefined}),
%% Listen on the service port for owner
- {ok, #{ module := OwnerModule }} = automate_service_registry:get_service_by_id(
- ServicePortId, TargetUserId),
- {ok, OwnerChannelId } = automate_service_registry_query:get_monitor_id(
- OwnerModule, OwnerUserId),
- ok = automate_channel_engine:listen_channel(OwnerChannelId),
+ {ok, _} = ?UTILS:establish_connection(ServicePortId, OwnerUserId),
+ ok = automate_service_registry_query:listen_service(ServicePortId, OwnerUserId, {undefined, undefined}),
%% Emit notification
{ok, ExpectedContent} = emit_notification(ServicePortId, OwnerUserId,
null, #{ <<"test">> => 4 }),
%% Get notification twice
- receive {channel_engine, NonOwnerChannelId, NonOwnerReceivedMessage} ->
+ receive {channel_engine, _NonOwnerChannelId, NonOwnerReceivedMessage} ->
?assertEqual(ExpectedContent, NonOwnerReceivedMessage)
after ?RECEIVE_TIMEOUT ->
ct:fail(notif_to_all_users_non_owner_not_received)
end,
- receive {channel_engine, OwnerChannelId, OwnerReceivedMessage} ->
+ receive {channel_engine, _OwnerChannelId, OwnerReceivedMessage} ->
?assertEqual(ExpectedContent, OwnerReceivedMessage)
after ?RECEIVE_TIMEOUT ->
ct:fail(notif_to_all_users_owner_not_received)
end.
+
+%%====================================================================
+%% Configuration
+%%====================================================================
+set_public_bridge_to_private_owner() ->
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ ok = ?UTILS:set_admin_user(OwnerUserId),
+ {_, RequesterUserId} = ?UTILS:create_random_user(),
+ ServicePortName = <>,
+ {ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
+
+ %% Configure service port
+ Configuration = #{ <<"is_public">> => true
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ %% Found Owner and externals
+ ?assertMatch({ok, #{ ServicePortId := _ }}, automate_service_registry:get_all_services_for_user(OwnerUserId)),
+ ?assertMatch({ok, #{ ServicePortId := _ }}, automate_service_registry:get_all_services_for_user(RequesterUserId)),
+
+ %% Set to private
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => #{ <<"is_public">> => false
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ }
+ }),
+ %% Still found for owner
+ ?assertMatch({ok, #{ ServicePortId := _ }}, automate_service_registry:get_all_services_for_user(OwnerUserId)),
+
+ %% Not found for externals
+ {ok, Results} = ?APPLICATION:list_custom_blocks(RequesterUserId),
+ ?assertEqual(#{}, Results).
+
+set_private_bridge_to_public_owner() ->
+ {_, OwnerUserId} = ?UTILS:create_random_user(),
+ ok = ?UTILS:set_admin_user(OwnerUserId),
+ {_, TargetUserId} = ?UTILS:create_random_user(),
+ ServicePortName = <>,
+ {ok, ServicePortId} = ?APPLICATION:create_service_port(OwnerUserId, ServicePortName),
+
+ %% Configure service port
+ Configuration = #{ <<"is_public">> => false
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ },
+
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => Configuration
+ }),
+
+ %% Found for owner, but NOT for externals
+ ?assertMatch({ok, #{ ServicePortId := _ }}, automate_service_registry:get_all_services_for_user(OwnerUserId)),
+ ?assertNotMatch({ok, #{ ServicePortId := _ }}, automate_service_registry:get_all_services_for_user(TargetUserId)),
+
+ %% Set to public
+ ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
+ #{ <<"type">> => <<"CONFIGURATION">>
+ , <<"value">> => #{ <<"is_public">> => true
+ , <<"service_name">> => ServicePortName
+ , <<"blocks">> => [ ]
+ }
+ }),
+ %% Found on owner AND on externals
+ ?assertMatch({ok, #{ ServicePortId := _ }}, automate_service_registry:get_all_services_for_user(OwnerUserId)),
+ ?assertMatch({ok, #{ ServicePortId := _ }}, automate_service_registry:get_all_services_for_user(TargetUserId)).
+
+
%%====================================================================
%% Notification routing tests - Internal functions
%%====================================================================
@@ -389,19 +566,20 @@ emit_notification(ServicePortId, OwnerUserId, TargetUserId, Content) ->
ToUserId = case TargetUserId of
null -> null;
_ ->
- {ok, ObfuscatedUserId} =
- ?APPLICATION:internal_user_id_to_service_port_user_id(TargetUserId,
- ServicePortId),
- ObfuscatedUserId
+ {ok, ConnectionId} = ?APPLICATION:internal_user_id_to_connection_id(TargetUserId,
+ ServicePortId),
+ ConnectionId
end,
ok = ?APPLICATION:from_service_port(ServicePortId, OwnerUserId,
- jiffy:encode(#{ <<"type">> => <<"NOTIFICATION">>
- , <<"key">> => Key
- , <<"to_user">> => ToUserId
- , <<"value">> => Value
- , <<"content">> => Content
- })),
+ #{ <<"type">> => <<"NOTIFICATION">>
+ , <<"key">> => Key
+ , <<"to_user">> => ToUserId
+ , <<"value">> => Value
+ , <<"content">> => Content
+ }),
{ok, #{ <<"content">> => Content
, <<"key">> => Key
, <<"value">> => Value
+ , <<"subkey">> => undefined
+ , <<"service_id">> => ServicePortId
}}.
diff --git a/backend/apps/automate_service_registry/src/automate_service_registry.erl b/backend/apps/automate_service_registry/src/automate_service_registry.erl
index 2eb5301e..fd937607 100644
--- a/backend/apps/automate_service_registry/src/automate_service_registry.erl
+++ b/backend/apps/automate_service_registry/src/automate_service_registry.erl
@@ -8,11 +8,12 @@
%% API
-export([ get_all_public_services/0
, get_all_services_for_user/1
- , get_service_by_id/2
+ , get_service_by_id/1
, register_public/1
, register_private/1
, update_service_module/3
, allow_user/2
+ , update_visibility/2
, get_config_for_service/2
, set_config_for_service/3
@@ -33,17 +34,17 @@
%%====================================================================
%% API functions
%%====================================================================
--spec get_all_public_services() -> {ok, service_info_map()} | {error, term(), string()}.
+-spec get_all_public_services() -> {ok, service_info_map()}.
get_all_public_services() ->
?BACKEND:list_all_public().
--spec get_all_services_for_user(binary()) -> {ok, service_info_map()} | {error, term(), string()}.
-get_all_services_for_user(UserId) ->
- ?BACKEND:get_all_services_for_user(UserId).
+-spec get_all_services_for_user(owner_id()) -> {ok, service_info_map()}.
+get_all_services_for_user(Owner) ->
+ ?BACKEND:get_all_services_for_user(Owner).
--spec get_service_by_id(binary(), binary()) -> {ok, service_entry()} | {error, not_found}.
-get_service_by_id(ServiceId, UserId) ->
- ?BACKEND:get_service_by_id(ServiceId, UserId).
+-spec get_service_by_id(binary()) -> {ok, service_entry()} | {error, not_found}.
+get_service_by_id(ServiceId) ->
+ ?BACKEND:get_service_by_id(ServiceId).
-spec register_public(module() | ?MODULE_MAP) -> {ok, binary()}.
register_public(ServiceModule) ->
@@ -54,7 +55,7 @@ register_public(ServiceModule) ->
{ok, Uuid}.
-spec update_service_module(module() | ?MODULE_MAP,
- binary(), binary()) -> ok.
+ binary(), owner_id()) -> ok.
update_service_module(Module, _ServiceId, _OwnerId) ->
{Uuid, Data} = module_to_map(Module),
?BACKEND:update_service_module(Uuid, Data).
@@ -67,15 +68,19 @@ register_private(ServiceModule) ->
Data),
{ok, Uuid}.
--spec allow_user(binary(), binary()) -> ok | {error, service_not_found}.
-allow_user(ServiceId, UserId) ->
- ?BACKEND:allow_user(ServiceId, UserId).
+-spec allow_user(binary(), owner_id()) -> ok | {error, service_not_found}.
+allow_user(ServiceId, Owner) ->
+ ?BACKEND:allow_user(ServiceId, Owner).
+
+-spec update_visibility(binary(), boolean()) -> ok | {error, service_not_found}.
+update_visibility(ServiceId, IsPublic) ->
+ ?BACKEND:update_visibility(ServiceId, IsPublic).
-spec get_config_for_service(binary(), atom()) -> {ok, any()} | {error, not_found}.
get_config_for_service(ServiceId, Property) ->
?BACKEND:get_config_for_service(ServiceId, Property).
--spec set_config_for_service(binary(), atom(), any()) -> ok.
+-spec set_config_for_service(binary(), atom(), any()) -> ok | {error, atom()}.
set_config_for_service(ServiceId, Property, Value) ->
?BACKEND:set_config_for_service(ServiceId, Property, Value).
@@ -83,9 +88,9 @@ set_config_for_service(ServiceId, Property, Value) ->
count_all_services() ->
?BACKEND:count_all_services().
--spec delete_service(binary(), binary()) -> ok.
-delete_service(UserId, ServiceId) ->
- ?BACKEND:delete_service(UserId, ServiceId).
+-spec delete_service(owner_id(), binary()) -> ok.
+delete_service(Owner, ServiceId) ->
+ ?BACKEND:delete_service(Owner, ServiceId).
diff --git a/backend/apps/automate_service_registry/src/automate_service_registry_configuration.erl b/backend/apps/automate_service_registry/src/automate_service_registry_configuration.erl
index 3c0521cb..06ace80c 100644
--- a/backend/apps/automate_service_registry/src/automate_service_registry_configuration.erl
+++ b/backend/apps/automate_service_registry/src/automate_service_registry_configuration.erl
@@ -31,5 +31,46 @@ get_versioning(_Nodes) ->
#database_version_progression
{ base=Version_1
- , updates=[]
+ , updates=[ #database_version_transformation
+ %% Fix old registry record name
+ { id=1
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?SERVICE_CONFIGURATION_TABLE,
+ fun(Old) ->
+ case Old of
+ %% If record name was set incorrectly, fix it
+ {service_configuration_entry, ConfigId, Value} ->
+ {services_configuration_entry, ConfigId, Value};
+ _ ->
+ Old %% Keep old record
+ end
+ end,
+ [ configuration_id, value ],
+ services_configuration_entry
+ )
+ end
+ }
+ %% Introduce user groups
+ , #database_version_transformation
+ { id=2
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_SERVICE_ALLOWANCE_TABLE,
+ fun({ user_service_allowance_entry
+ , ServiceId, UserId
+ }) ->
+ { user_service_allowance_entry
+ , ServiceId, {user, UserId}
+ }
+ end,
+ [ sevice_id, owner ],
+ user_service_allowance_entry
+ ),
+
+ ok = mnesia:wait_for_tables([ ?USER_SERVICE_ALLOWANCE_TABLE ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+ ]
}.
diff --git a/backend/apps/automate_service_registry/src/automate_service_registry_mnesia_backend.erl b/backend/apps/automate_service_registry/src/automate_service_registry_mnesia_backend.erl
index 1f5e182b..0865a8c2 100644
--- a/backend/apps/automate_service_registry/src/automate_service_registry_mnesia_backend.erl
+++ b/backend/apps/automate_service_registry/src/automate_service_registry_mnesia_backend.erl
@@ -10,7 +10,8 @@
, list_all_public/0
, get_all_services_for_user/1
, allow_user/2
- , get_service_by_id/2
+ , get_service_by_id/1
+ , update_visibility/2
, update_service_module/2
, get_config_for_service/2
@@ -77,7 +78,7 @@ update_service_module(Uuid, #{ name := Name
{error, Reason, mnesia:error_description(Reason)}
end.
--spec list_all_public() -> {ok, service_info_map()} | {error, term(), string()}.
+-spec list_all_public() -> {ok, service_info_map()}.
list_all_public() ->
MatchHead = #services_table_entry{ id='_'
, public='$1'
@@ -94,19 +95,17 @@ list_all_public() ->
mnesia:select(?SERVICE_REGISTRY_TABLE, Matcher)
end,
- case mnesia:transaction(Transaction) of
- {atomic, Result} ->
- {ok, convert_to_map(Result)};
- {aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ case mnesia:ets(Transaction) of
+ Result ->
+ {ok, convert_to_map(Result)}
end.
--spec allow_user(binary(), binary()) -> ok | {error, service_not_found}.
-allow_user(ServiceId, UserId) ->
+-spec allow_user(binary(), owner_id()) -> ok | {error, service_not_found}.
+allow_user(ServiceId, Owner) ->
Transaction = fun() ->
ok = mnesia:write(?USER_SERVICE_ALLOWANCE_TABLE,
#user_service_allowance_entry{ service_id=ServiceId
- , user_id=UserId
+ , owner=Owner
},
write)
end,
@@ -117,8 +116,27 @@ allow_user(ServiceId, UserId) ->
{error, Reason, mnesia:error_description(Reason)}
end.
--spec get_all_services_for_user(binary()) -> {ok, service_info_map()} | {error, term(), string()}.
-get_all_services_for_user(UserId) ->
+-spec update_visibility(binary(), boolean()) -> ok | {error, service_not_found}.
+update_visibility(ServiceId, IsPublic) ->
+ Transaction = fun() ->
+ case mnesia:read(?SERVICE_REGISTRY_TABLE, ServiceId) of
+ [Entry] ->
+ ok = mnesia:write(?SERVICE_REGISTRY_TABLE,
+ Entry#services_table_entry{ public=IsPublic
+ }, write);
+ [] ->
+ {error, service_not_found}
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ {error, Reason}
+ end.
+
+-spec get_all_services_for_user(owner_id()) -> {ok, service_info_map()}.
+get_all_services_for_user({OwnerType, OwnerId}) ->
MatchHead = #services_table_entry{ id='_'
, public='$1'
, name='_'
@@ -130,13 +148,18 @@ get_all_services_for_user(UserId) ->
ResultColumn = '$_',
PublicMatcher = [{MatchHead, Guards, [ResultColumn]}],
- AllowancesMatcherHead = #user_service_allowance_entry{ service_id='$1', user_id='$2' },
- AllowancesGuards = [{'==', '$2', UserId }],
+ AllowancesMatcherHead = #user_service_allowance_entry{ service_id='$1', owner={'$2', '$3'} },
+ AllowancesGuards = [ {'==', '$2', OwnerType }
+ , {'==', '$3', OwnerId }
+ ],
AllowancesResultColumn = '$1',
AllowancesMatcher = [{AllowancesMatcherHead, AllowancesGuards, [AllowancesResultColumn]}],
Transaction = fun() ->
+ %% Public programs
Public = mnesia:select(?SERVICE_REGISTRY_TABLE, PublicMatcher),
+
+ %% Programs where the user is allowed
UserAllowanceIds = mnesia:select(?USER_SERVICE_ALLOWANCE_TABLE, AllowancesMatcher),
UserAllowances = lists:filtermap(fun (ServiceId) ->
case mnesia:read(?SERVICE_REGISTRY_TABLE, ServiceId) of
@@ -146,18 +169,58 @@ get_all_services_for_user(UserId) ->
{true, Result}
end
end, UserAllowanceIds),
- {Public, UserAllowances}
+
+ %% Programs from a user group where it has the role needed to use them
+ GroupAllowances = case OwnerType of
+ group -> []; %% Right now a group cannot be part of another group
+ user ->
+ {ok, UserGroups} = automate_storage:get_user_groups({user, OwnerId}),
+
+ %% Obtain groups from where the user is allowed to take connections from
+ AllowedInGroups = lists:filtermap(
+ fun({#user_group_entry{ id=GroupId
+ , min_level_for_private_bridge_usage=MinLevel }
+ , Role}) ->
+ case automate_storage_utils:role_has_min_level_in_group(Role, MinLevel) of
+ false -> false;
+ true -> { true, GroupId }
+ end
+ end, UserGroups),
+
+ %% Find the allowances of the groups found
+ lists:flatmap(fun(GroupId) ->
+ GroupHead = #user_service_allowance_entry{ service_id='$1'
+ , owner={'$2', '$3'}
+ },
+ GroupGuard = [ {'==', '$2', group }
+ , {'==', '$3', GroupId }
+ ],
+ GroupResultColumn = '$1',
+ GroupMatcher = [{GroupHead, GroupGuard, [GroupResultColumn]}],
+ AllowanceIds = mnesia:select(?USER_SERVICE_ALLOWANCE_TABLE, GroupMatcher),
+ Allowances = lists:filtermap(
+ fun (ServiceId) ->
+ case mnesia:read(?SERVICE_REGISTRY_TABLE, ServiceId) of
+ [] ->
+ false;
+ [Result] ->
+ {true, Result}
+ end
+ end, AllowanceIds),
+ Allowances
+ end, AllowedInGroups)
+ end,
+
+ {Public, UserAllowances, GroupAllowances}
end,
- case mnesia:transaction(Transaction) of
- {atomic, {Public, UserAllowances}} ->
- {ok, convert_to_map(Public ++ UserAllowances)};
- {aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ case mnesia:ets(Transaction) of
+ {Public, UserAllowances, GroupAllowances} ->
+ {ok, convert_to_map(Public ++ UserAllowances ++ GroupAllowances)}
end.
--spec get_service_by_id(binary(), binary()) -> {ok, service_entry()} | {error, not_found}.
-get_service_by_id(ServiceId, _UserId) ->
+-spec get_service_by_id(binary()) -> {ok, service_entry()} | {error, not_found}.
+get_service_by_id(ServiceId) ->
Transaction = fun() ->
%% TODO: Check user permissions
case mnesia:read(?SERVICE_REGISTRY_TABLE, ServiceId) of
@@ -167,13 +230,11 @@ get_service_by_id(ServiceId, _UserId) ->
{ok, Result}
end
end,
- case mnesia:transaction(Transaction) of
- {atomic, {ok, Result}} ->
+ case mnesia:ets(Transaction) of
+ {ok, Result} ->
{ok, entry_to_map(Result)};
- {atomic, Result} ->
- Result;
- {aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ Result ->
+ Result
end.
@@ -184,31 +245,26 @@ get_config_for_service(ServiceId, Property) ->
case mnesia:read(?SERVICE_CONFIGURATION_TABLE, {ServiceId, Property}) of
[] ->
{error, not_found};
- [#service_configuration_entry{value=Value}] ->
+ [#services_configuration_entry{value=Value}] ->
{ok, Value}
end
end,
- case mnesia:transaction(Transaction) of
- {atomic, Result} ->
- Result;
- {aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
- end.
+ mnesia:ets(Transaction).
--spec set_config_for_service(binary(), atom(), any()) -> ok.
+-spec set_config_for_service(binary(), atom(), any()) -> ok | {error, atom()}.
set_config_for_service(ServiceId, Property, Value) ->
Transaction = fun() ->
%% TODO: Check user permissions
- mnesia:write(?SERVICE_CONFIGURATION_TABLE,
- #service_configuration_entry{ configuration_id={ServiceId, Property}
- , value=Value
- }, write)
+ ok = mnesia:write(?SERVICE_CONFIGURATION_TABLE,
+ #services_configuration_entry{ configuration_id={ServiceId, Property}
+ , value=Value
+ }, write)
end,
case mnesia:transaction(Transaction) of
{atomic, Result} ->
Result;
{aborted, Reason} ->
- {error, Reason, mnesia:error_description(Reason)}
+ {error, Reason}
end.
-spec count_all_services() -> number().
@@ -216,8 +272,9 @@ count_all_services() ->
length(mnesia:dirty_all_keys(?SERVICE_REGISTRY_TABLE)).
--spec delete_service(binary(), binary()) -> ok.
-delete_service(_UserId, ServiceId) ->
+-spec delete_service(owner_id(), binary()) -> ok.
+delete_service(_Owner, ServiceId) ->
+ %% HACK: Note that service registry table does not capture owners
Transaction = fun() ->
ok = mnesia:delete(?SERVICE_REGISTRY_TABLE, ServiceId, write)
end,
diff --git a/backend/apps/automate_service_registry/src/automate_service_registry_query.erl b/backend/apps/automate_service_registry/src/automate_service_registry_query.erl
index 258c03bd..d0473aee 100644
--- a/backend/apps/automate_service_registry/src/automate_service_registry_query.erl
+++ b/backend/apps/automate_service_registry/src/automate_service_registry_query.erl
@@ -9,8 +9,8 @@
-export([ is_enabled_for_user/2
, get_how_to_enable/2
, call/5
- , get_monitor_id/2
- , send_registration_data/3
+ , send_registration_data/4
+ , listen_service/3
]).
-define(SERVER, ?MODULE).
@@ -21,33 +21,40 @@
%%====================================================================
%% API functions
%%====================================================================
-is_enabled_for_user({Module, Params}, Username) ->
- Module:is_enabled_for_user(Username, Params);
+-spec is_enabled_for_user(module() | {module(), any()}, owner_id()) -> {ok, boolean()}.
+is_enabled_for_user({Module, Params}, Owner) ->
+ Module:is_enabled_for_user(Owner, Params);
is_enabled_for_user(Module, Username) ->
Module:is_enabled_for_user(Username).
+-spec get_how_to_enable(module() | {module(), [_]}, owner_id()) -> {ok, map()} | {error, not_found} | {error, no_connection} | {error, _}.
get_how_to_enable({Module, Params}, UserInfo) ->
Module:get_how_to_enable(UserInfo, Params);
get_how_to_enable(Module, UserInfo) ->
Module:get_how_to_enable(UserInfo).
--spec call(module() | {module(), any()}, binary(), any(), #program_thread{}, binary()) -> {ok, #program_thread{}, any()}.
-call({Module, Params}, Action, Values, Thread, UserId) ->
- Module:call(Action, Values, Thread, UserId, Params);
-
-call(Module, Action, Values, Thread, UserId) ->
- Module:call(Action, Values, Thread, UserId).
-
-get_monitor_id({Module, Params}, UserId) ->
- Module:get_monitor_id(UserId, Params);
-
-get_monitor_id(Module, UserId) ->
- Module:get_monitor_id(UserId).
-
-send_registration_data({Module, Params}, UserId, RegistrationData) ->
- Module:send_registration_data(UserId, RegistrationData, Params);
-
-send_registration_data(Module, UserId, RegistrationData) ->
- Module:send_registration_data(UserId, RegistrationData).
+-spec call(module() | {module(), any()}, binary(), any(), #program_thread{}, owner_id()) -> {ok, #program_thread{}, any()} | {error, no_connection | {failed, _} | timeout | no_valid_connection | {error_getting_resource, _}} .
+call({Module, Params}, Action, Values, Thread, Owner) ->
+ Module:call(Action, Values, Thread, Owner, Params);
+
+call(Module, Action, Values, Thread, Owner) ->
+ Module:call(Action, Values, Thread, Owner).
+
+-spec send_registration_data(module() | {module(), any()}, owner_id(), any(), any()) -> {ok, any()}.
+send_registration_data({Module, Params}, UserId, RegistrationData, RegistrationProperties) ->
+ Module:send_registration_data(UserId, RegistrationData, Params, RegistrationProperties);
+
+send_registration_data(Module, UserId, RegistrationData, RegistrationProperties) ->
+ Module:send_registration_data(UserId, RegistrationData, [], RegistrationProperties).
+
+-spec listen_service(ServiceId :: binary(), Owner :: owner_id(), { any(), any() }) -> ok | {error, any()}.
+listen_service(ServiceId, Owner, { Key, SubKey }) ->
+ {ok, #{ module := Module }} = automate_service_registry:get_service_by_id(ServiceId),
+ case Module of
+ {ParametrizedModule, Params} ->
+ ParametrizedModule:listen_service(Owner, {Key, SubKey}, Params);
+ _ ->
+ Module:listen_service(Owner, {Key, SubKey})
+ end.
diff --git a/backend/apps/automate_service_registry/src/records.hrl b/backend/apps/automate_service_registry/src/records.hrl
index 7a319b86..6ad0058f 100644
--- a/backend/apps/automate_service_registry/src/records.hrl
+++ b/backend/apps/automate_service_registry/src/records.hrl
@@ -1,4 +1,5 @@
-define(PRIMITIVE_TYPES, boolean() | binary() | number()).
+-include("../../automate_storage/src/records.hrl").
-type call_type() :: 'string' | 'positive' | 'integer' | 'list' | 'boolean'.
@@ -12,25 +13,23 @@
, parameters :: [#parameter{}]
}).
--define(SELECTOR_VALUES, '_' | '$1' | '$2').
-
--record(services_table_entry, { id :: binary() | ?SELECTOR_VALUES
- , public :: boolean() | ?SELECTOR_VALUES
- , name :: binary() | ?SELECTOR_VALUES
- , description :: binary() | ?SELECTOR_VALUES
- , module :: module() | {module(), [_]} | ?SELECTOR_VALUES
+-record(services_table_entry, { id :: binary() | ?MNESIA_SELECTOR
+ , public :: boolean() | ?MNESIA_SELECTOR
+ , name :: binary() | ?MNESIA_SELECTOR
+ , description :: binary() | ?MNESIA_SELECTOR
+ , module :: module() | {module(), [_]} | ?MNESIA_SELECTOR
}).
-type service_entry() :: #{ name := binary()
, description := binary()
- , module := module()
+ , module := module() | {module(), [_]}
}.
-type service_info_map() :: #{ binary() := service_entry() }.
--record(user_service_allowance_entry, { service_id :: binary() | ?SELECTOR_VALUES
- , user_id :: binary() | ?SELECTOR_VALUES
+-record(user_service_allowance_entry, { service_id :: binary() | ?MNESIA_SELECTOR
+ , owner :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
}).
--record(service_configuration_entry, { configuration_id :: { binary(), atom() } %% Service id, propery
- , value :: any()
- }).
+-record(services_configuration_entry, { configuration_id :: { binary(), atom() } %% Service id, property
+ , value :: any()
+ }).
diff --git a/backend/apps/automate_service_registry/test/automate_service_registry_tests.erl b/backend/apps/automate_service_registry/test/automate_service_registry_tests.erl
index ab729738..ba58d8c0 100644
--- a/backend/apps/automate_service_registry/test/automate_service_registry_tests.erl
+++ b/backend/apps/automate_service_registry/test/automate_service_registry_tests.erl
@@ -7,8 +7,8 @@
-define(APPLICATION, automate_service_registry).
-define(TEST_NODES, [node()]).
--define(ALLOWED_USER, <<"allowed user">>).
--define(UNAUTHORISED_USER, <<"unauthorised user">>).
+-define(ALLOWED_USER, {user, <<"allowed user">>}).
+-define(UNAUTHORISED_USER, {user, <<"unauthorised user">>}).
%%====================================================================
%% Test API
diff --git a/backend/apps/automate_service_user_registration/src/automate_service_user_registration.app.src b/backend/apps/automate_service_user_registration/src/automate_service_user_registration.app.src
deleted file mode 100644
index 5000ad42..00000000
--- a/backend/apps/automate_service_user_registration/src/automate_service_user_registration.app.src
+++ /dev/null
@@ -1,17 +0,0 @@
-{ application, automate_service_user_registration,
- [
- {description, "Auto-mate user registration service."},
- {vsn, "0.0.0"},
- {registered, []},
- {mod, { automate_service_user_registration_app, [] }},
- {applications, [ kernel
- , stdlib
- , automate_storage
- , automate_configuration
- ]},
- {env, [
- ]},
- {modules, []},
- {licenses, ["Apache 2.0"]},
- {links, []}
- ]}.
diff --git a/backend/apps/automate_service_user_registration/src/automate_service_user_registration.erl b/backend/apps/automate_service_user_registration/src/automate_service_user_registration.erl
deleted file mode 100644
index f7f4b9b0..00000000
--- a/backend/apps/automate_service_user_registration/src/automate_service_user_registration.erl
+++ /dev/null
@@ -1,35 +0,0 @@
--module(automate_service_user_registration).
-
--define(BACKEND, automate_service_user_registration_backend_mnesia).
-
--export([ start_link/0
- , get_info_from_registration_token/1
- , get_or_gen_registration_token/2
- ]).
-
-
--include("records.hrl").
-%%====================================================================
-%% API functions
-%%====================================================================
-
--spec start_link() -> {ok, pid()}.
-start_link() ->
- ignore = ?BACKEND:start_link(),
- {ok, self()}.
-
--spec get_info_from_registration_token(binary()) -> {ok, #service_registration_token{}} | {error, not_found}.
-get_info_from_registration_token(Token) ->
- ?BACKEND:get_info_from_registration_token(Token).
-
--spec get_or_gen_registration_token(binary(), binary()) -> {ok, binary()}.
-get_or_gen_registration_token(Username, ServiceId) ->
- case ?BACKEND:get_registration_token(Username, ServiceId) of
- {ok, Token} ->
- {ok, Token};
- {error, not_found} ->
- case ?BACKEND:gen_registration_token(Username, ServiceId) of
- {ok, Token} ->
- {ok, Token}
- end
- end.
diff --git a/backend/apps/automate_service_user_registration/src/automate_service_user_registration_backend_mnesia.erl b/backend/apps/automate_service_user_registration/src/automate_service_user_registration_backend_mnesia.erl
deleted file mode 100644
index 5954b6cc..00000000
--- a/backend/apps/automate_service_user_registration/src/automate_service_user_registration_backend_mnesia.erl
+++ /dev/null
@@ -1,105 +0,0 @@
--module(automate_service_user_registration_backend_mnesia).
-
--export([ start_link/0
- , gen_registration_token/2
- , get_registration_token/2
- , get_info_from_registration_token/1
- ]).
-
--include("records.hrl").
--include("../../automate_storage/src/records.hrl").
--include("databases.hrl").
-
-%%====================================================================
-%% API functions
-%%====================================================================
-start_link() ->
- Nodes = automate_configuration:get_sync_peers(),
-
- ok = automate_storage_versioning:apply_versioning(
- automate_service_user_registration_configuration:get_versioning(Nodes),
- Nodes, ?MODULE),
-
- ignore.
-
-
--spec get_registration_token(binary(), binary()) -> {ok, binary()} | { error, not_found }.
-get_registration_token(Username, ServiceId) ->
- Transaction = fun() ->
- case automate_storage:get_userid_from_username(Username) of
- {ok, UserId} ->
- TokenMatchHead = #service_registration_token{ token='$1'
- , service_id='$2'
- , user_id='$3'
- },
-
- %% Check that neither the id, username or email matches another
- GuardService = {'==', '$2', ServiceId},
- GuardUserId = {'==', '$3', UserId},
- TokenGuard = {'andthen', GuardService, GuardUserId},
- TokenResultColumn = '$1',
- TokenMatcher = [{TokenMatchHead, [TokenGuard], [TokenResultColumn]}],
-
- case mnesia:select(?SERVICE_REGISTRATION_TOKEN_TABLE, TokenMatcher) of
- [Token] ->
- { ok, Token };
- [] ->
- {error, not_found}
- end;
- {error, no_user_found} ->
- {error, not_found}
- end
- end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
- end.
-
--spec gen_registration_token(binary(), binary()) -> {ok, binary()}.
-gen_registration_token(Username, ServiceId) ->
- Token = generate_id(),
-
- Transaction = fun() ->
- case automate_storage:get_userid_from_username(Username) of
- {ok, UserId} ->
- TokenRegistration = #service_registration_token{ token=Token
- , service_id=ServiceId
- , user_id=UserId
- },
- ok = mnesia:write(?SERVICE_REGISTRATION_TOKEN_TABLE, TokenRegistration, write),
- {ok, Token};
- {error, no_user_found} ->
- {ok, not_found}
- end
- end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
- end.
-
--spec get_info_from_registration_token(binary()) -> {ok, #service_registration_token{}} | {error, not_found}.
-get_info_from_registration_token(Token) ->
- Transaction = fun () ->
- mnesia:read(?SERVICE_REGISTRATION_TOKEN_TABLE, Token)
- end,
- case mnesia:transaction(Transaction) of
- { atomic, [] } ->
- {error, not_found};
- { atomic, [Result = #service_registration_token{}] } ->
- {ok, Result};
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
- end.
-
-
-
-
-%%====================================================================
-%% Internal functions
-%%====================================================================
-generate_id() ->
- binary:list_to_bin(uuid:to_string(uuid:uuid4())).
diff --git a/backend/apps/automate_service_user_registration/src/automate_service_user_registration_configuration.erl b/backend/apps/automate_service_user_registration/src/automate_service_user_registration_configuration.erl
deleted file mode 100644
index 8ce620f0..00000000
--- a/backend/apps/automate_service_user_registration/src/automate_service_user_registration_configuration.erl
+++ /dev/null
@@ -1,25 +0,0 @@
-%%%-------------------------------------------------------------------
-%% @doc automate service user configuration's configuration and versioning
-%% @end
-%%%-------------------------------------------------------------------
--module(automate_service_user_registration_configuration).
-
--export([ get_versioning/1
- ]).
-
--include("databases.hrl").
--include("../../automate_storage/src/versioning.hrl").
-
--spec get_versioning([node()]) -> #database_version_progression{}.
-get_versioning(_Nodes) ->
- %% Service registration token table
- Version_1 = [ #database_version_data{ database_name=?SERVICE_REGISTRATION_TOKEN_TABLE
- , records=[ token, service_id, user_id ]
- , record_name=service_registration_token
- }
- ],
-
- #database_version_progression
- { base=Version_1
- , updates=[]
- }.
diff --git a/backend/apps/automate_service_user_registration/src/databases.hrl b/backend/apps/automate_service_user_registration/src/databases.hrl
deleted file mode 100644
index ec9b807b..00000000
--- a/backend/apps/automate_service_user_registration/src/databases.hrl
+++ /dev/null
@@ -1 +0,0 @@
--define(SERVICE_REGISTRATION_TOKEN_TABLE, automate_service_registration_token_table).
diff --git a/backend/apps/automate_service_user_registration/src/records.hrl b/backend/apps/automate_service_user_registration/src/records.hrl
deleted file mode 100644
index 3dcd2236..00000000
--- a/backend/apps/automate_service_user_registration/src/records.hrl
+++ /dev/null
@@ -1,4 +0,0 @@
--record(service_registration_token, { token :: binary() | '_' | '$1' | '$2' | '$3'
- , service_id :: binary() | '_' | '$1' | '$2' | '$3'
- , user_id :: binary() | '_' | '$1' | '$2' | '$3'
- }).
diff --git a/backend/apps/automate_services_all/src/automate_services_all.app.src b/backend/apps/automate_services_all/src/automate_services_all.app.src
deleted file mode 100644
index 08689051..00000000
--- a/backend/apps/automate_services_all/src/automate_services_all.app.src
+++ /dev/null
@@ -1,13 +0,0 @@
-{application, automate_services_all, [
- {description, "Auto-mate services wrapper."},
- {vsn, "0.0.0"},
- {registered, []},
- {applications, [ automate_configuration
- , automate_services_time
- ]},
- {env, [
- ]},
- {modules, []},
- {licenses, ["Apache 2.0"]},
- {links, []}
-]}.
diff --git a/backend/apps/automate_services_time/src/automate_services_time.app.src b/backend/apps/automate_services_time/src/automate_services_time.app.src
index da2d6c1d..8cc46bdd 100644
--- a/backend/apps/automate_services_time/src/automate_services_time.app.src
+++ b/backend/apps/automate_services_time/src/automate_services_time.app.src
@@ -8,6 +8,8 @@
, automate_channel_engine
, automate_monitor_engine
, automate_configuration
+ , automate_coordination
+ , qdate
]},
{env, [
]},
diff --git a/backend/apps/automate_services_time/src/automate_services_time.erl b/backend/apps/automate_services_time/src/automate_services_time.erl
index 9fda0615..0296105d 100644
--- a/backend/apps/automate_services_time/src/automate_services_time.erl
+++ b/backend/apps/automate_services_time/src/automate_services_time.erl
@@ -10,13 +10,16 @@
, get_description/0
, get_uuid/0
, get_name/0
+ , get_monitor_id/0
, is_enabled_for_user/1
, get_how_to_enable/1
- , get_monitor_id/1
+ , listen_service/2
, call/4
]).
+-include("./definitions.hrl").
-include("../../automate_channel_engine/src/records.hrl").
+-include("../../automate_testing/src/testing.hrl").
-define(SLEEP_RESOULUTION_MS, 500).
%%====================================================================
@@ -29,7 +32,7 @@ start_link() ->
%% This one can be considered static.
get_uuid() ->
- <<"0093325b-373f-4f1c-bace-4532cce79df4">>.
+ ?TIME_SERVICE_UUID.
get_name() ->
<<"Timekeeping">>.
@@ -37,10 +40,10 @@ get_name() ->
get_description() ->
<<"Timekeeping service.">>.
-%% No monitor associated with this service
-get_monitor_id(_UserId) ->
+get_monitor_id() ->
case automate_service_registry:get_config_for_service(get_uuid(), monitor_id) of
{error, not_found} ->
+ %% No monitor associated with this service
{ok, ChannelId} = automate_channel_engine:create_channel(),
automate_service_registry:set_config_for_service(get_uuid(), monitor_id, ChannelId),
{ok, ChannelId};
@@ -48,20 +51,57 @@ get_monitor_id(_UserId) ->
{ok, ChannelId}
end.
+-spec listen_service(owner_id(), {_, _}) -> ok.
+listen_service(_Owner, _Selector) ->
+ {ok, ChannelId} = get_monitor_id(),
+ automate_channel_engine:listen_channel(ChannelId).
+
call(get_utc_hour, _Values, Thread, _UserId) ->
- {{_Y1970, _Mon, _Day}, {Hour, _Min, _Sec}} = calendar:now_to_datetime(erlang:timestamp()),
+ {{_Y1970, _Mon, _Day}, {Hour, _Min, _Sec}} = calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp())),
{ok, Thread, Hour};
call(get_utc_minute, _Values, Thread, _UserId) ->
- {{_Y1970, _Mon, _Day}, {_Hour, Min, _Sec}} = calendar:now_to_datetime(erlang:timestamp()),
+ {{_Y1970, _Mon, _Day}, {_Hour, Min, _Sec}} = calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp())),
{ok, Thread, Min};
call(get_utc_seconds, _Values, Thread, _UserId) ->
- {{_Y1970, _Mon, _Day}, {_Hour, _Min, Sec}} = calendar:now_to_datetime(erlang:timestamp()),
+ {{_Y1970, _Mon, _Day}, {_Hour, _Min, Sec}} = calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp())),
+ {ok, Thread, Sec};
+
+call(get_tz_hour, [Timezone], Thread, _UserId) ->
+ {{_Y1970, _Mon, _Day}, {Hour, _Min, _Sec}} = qdate:to_date(Timezone, prefer_standard, calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp()))),
+ {ok, Thread, Hour};
+
+call(get_tz_minute, [Timezone], Thread, _UserId) ->
+ {{_Y1970, _Mon, _Day}, {_Hour, Min, _Sec}} = qdate:to_date(Timezone, prefer_standard, calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp()))),
+ {ok, Thread, Min};
+
+call(get_tz_seconds, [Timezone], Thread, _UserId) ->
+ {{_Y1970, _Mon, _Day}, {_Hour, _Min, Sec}} = qdate:to_date(Timezone, prefer_standard, calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp()))),
{ok, Thread, Sec};
+call(get_tz_day_of_month, [Timezone], Thread, _UserId) ->
+ {{_Y1970, _Mon, Day}, {_Hour, _Min, _Sec}} = qdate:to_date(Timezone, prefer_standard, calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp()))),
+ {ok, Thread, Day};
+
+call(get_tz_month_of_year, [Timezone], Thread, _UserId) ->
+ {{_Y1970, Mon, _Day}, {_Hour, _Min, _Sec}} = qdate:to_date(Timezone, prefer_standard, calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp()))),
+ {ok, Thread, Mon};
+
+call(get_tz_year, [Timezone], Thread, _UserId) ->
+ {{Y1970, _Mon, _Day}, {_Hour, _Min, _Sec}} = qdate:to_date(Timezone, prefer_standard, calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp()))),
+ {ok, Thread, Y1970};
+
+call(get_tz_day_of_week, [_Timezone], Thread, _UserId) ->
+ {{Y1970, Mon, Day}, {_Hour, _Min, _Sec}} = calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp())),
+ %% Note that technically, calendar:day_of_the_week takes a Year, not Year1970 .
+ %% It should not affect this calculation, but keep it in mind.
+ %% See http://erlang.org/doc/man/calendar.html#type-year
+ DayOfWeek = calendar:day_of_the_week(Y1970, Mon, Day),
+ {ok, Thread, DayOfWeek};
+
call(<<"utc_is_day_of_week">>, [DayOfWeek], Thread, _UserId) ->
- {{Y1970, Mon, Day}, {_Hour, _Min, _Sec}} = calendar:now_to_datetime(erlang:timestamp()),
+ {{Y1970, Mon, Day}, {_Hour, _Min, _Sec}} = calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp())),
%% Note that technically, calendar:day_of_the_week takes a Year, not Year1970 .
%% It should not affect this calculation, but keep it in mind.
%% See http://erlang.org/doc/man/calendar.html#type-year
@@ -77,7 +117,7 @@ day_of_week_to_id(6) -> <<"sat">>;
day_of_week_to_id(7) -> <<"sun">>.
%% Is enabled for all users
-is_enabled_for_user(_Username) ->
+is_enabled_for_user(_Owner) ->
{ok, true}.
%% No need to enable service
@@ -89,32 +129,69 @@ get_how_to_enable(_) ->
%% Timekeeping service
%%====================================================================
spawn_timekeeper() ->
+ io:fwrite("[~p] Spawining Timekeeper~n", [node()]),
case automate_coordination:run_task_not_parallel(
fun() ->
- {ok, ChannelId} = get_monitor_id(none),
+ io:fwrite("[~p] Timekeeper started~n", [self()]),
+ {ok, ChannelId} = get_monitor_id(),
{ok, _} = automate_service_registry:register_public(automate_services_time),
- timekeeping_loop(ChannelId, {0, 0, 0})
+ timekeeping_loop(ChannelId, {{0, 0, 0}, {0, 0, 0}})
end, ?MODULE) of
{started, Pid} ->
+ link(Pid),
{ok, Pid};
{already_running, Pid} ->
+ link(Pid),
{ok, Pid};
{error, Error} ->
{error, Error}
end.
-timekeeping_loop(ChannelId, {LHour, LMin, LSec}) ->
- {_, {Hour, Min, Sec}} = calendar:now_to_datetime(erlang:timestamp()),
+timekeeping_loop(ChannelId, {{LYear, LMonth, LDay}, {LHour, LMin, LSec}}) ->
+ DateTime = calendar:now_to_datetime(?CORRECT_EXECUTION_TIME(erlang:timestamp())),
+ {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime,
case (Sec =/= LSec) orelse (Min =/= LMin) orelse (Hour =/= LHour) of
true ->
- %% automate_channel_engine:send_to_channel(ChannelId, {Hour, Min, Sec});
StrTime = binary:list_to_bin(lists:flatten(io_lib:format("~p:~p:~p", [Hour, Min, Sec]))),
+ GregorianSeconds = calendar:datetime_to_gregorian_seconds(DateTime),
automate_channel_engine:send_to_channel(ChannelId,
#{ ?CHANNEL_MESSAGE_CONTENT => StrTime
+ , <<"full">> => #{ <<"year">> => Year
+ , <<"month">> => Month
+ , <<"day">> => Day
+
+ , <<"hour">> => Hour
+ , <<"minute">> => Min
+ , <<"second">> => Sec
+ , <<"__as_gregorian_seconds">> => GregorianSeconds
+ }
+ , <<"as_list">> => [ Hour, Min, Sec]
+ , <<"key">> => <<"utc_time">>
+ , <<"service_id">> => get_uuid()
+ });
+ false ->
+ ok
+ end,
+ case (Year =/= LYear) orelse (Month =/= LMonth) orelse (Day =/= LDay) of
+ true ->
+ StrDate = binary:list_to_bin(lists:flatten(io_lib:format("~p/~p/~p", [Year, Month, Day]))),
+ automate_channel_engine:send_to_channel(ChannelId,
+ #{ ?CHANNEL_MESSAGE_CONTENT => StrDate
+ , <<"full">> => #{ <<"year">> => Year
+ , <<"month">> => Month
+ , <<"day">> => Day
+
+ , <<"hour">> => Hour
+ , <<"minute">> => Min
+ , <<"second">> => Sec
+ }
+ , <<"as_list">> => [ Year, Month, Day, calendar:day_of_the_week(Year, Month, Day)]
+ , <<"key">> => <<"utc_date">>
+ , <<"service_id">> => get_uuid()
});
false ->
ok
end,
timer:sleep(?SLEEP_RESOULUTION_MS), % Wait for less than a second
- timekeeping_loop(ChannelId, {Hour, Min, Sec}).
+ timekeeping_loop(ChannelId, {{Year, Month, Day}, {Hour, Min, Sec}}).
diff --git a/backend/apps/automate_services_time/src/automate_services_time_app.erl b/backend/apps/automate_services_time/src/automate_services_time_app.erl
index 01cd005c..7d721cba 100644
--- a/backend/apps/automate_services_time/src/automate_services_time_app.erl
+++ b/backend/apps/automate_services_time/src/automate_services_time_app.erl
@@ -8,14 +8,16 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
+start() ->
+ automate_services_time_sup:start_link().
start(_StartType, _StartArgs) ->
- automate_services_time:start_link().
+ start().
%%--------------------------------------------------------------------
stop(_State) ->
diff --git a/backend/apps/automate_services_time/src/automate_services_time_sup.erl b/backend/apps/automate_services_time/src/automate_services_time_sup.erl
new file mode 100644
index 00000000..8afeb5da
--- /dev/null
+++ b/backend/apps/automate_services_time/src/automate_services_time_sup.erl
@@ -0,0 +1,45 @@
+%%%-------------------------------------------------------------------
+%% @doc Timekeeping service supervisor.
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(automate_services_time_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+-define(SERVER, ?MODULE).
+-include("../../automate_common_types/src/definitions.hrl").
+
+%%====================================================================
+%% API functions
+%%====================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+
+%%====================================================================
+%% Supervisor callbacks
+%%====================================================================
+
+%% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules}
+init([]) ->
+ {ok, { {one_for_one, ?AUTOMATE_SUPERVISOR_INTENSITY, ?AUTOMATE_SUPERVISOR_PERIOD},
+ [ #{ id => automate_services_time
+ , start => {automate_services_time, start_link, []}
+ , restart => permanent
+ , shutdown => 2000
+ , type => worker
+ , modules => [automate_services_time]
+ }
+
+ ]} }.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
diff --git a/backend/apps/automate_services_time/src/definitions.hrl b/backend/apps/automate_services_time/src/definitions.hrl
new file mode 100644
index 00000000..f630126b
--- /dev/null
+++ b/backend/apps/automate_services_time/src/definitions.hrl
@@ -0,0 +1 @@
+-define(TIME_SERVICE_UUID, <<"0093325b-373f-4f1c-bace-4532cce79df4">>).
diff --git a/backend/apps/automate_stats/src/automate_stats.erl b/backend/apps/automate_stats/src/automate_stats.erl
index ea7e97f8..05da3001 100644
--- a/backend/apps/automate_stats/src/automate_stats.erl
+++ b/backend/apps/automate_stats/src/automate_stats.erl
@@ -11,11 +11,13 @@
, log_observation/3
, format/1
, remove_metric/2
+ , get_internal_metrics/0
]).
%% Internal calls
-export([ prepare/0
]).
+-include("./records.hrl").
-type metric_type() :: boolean | gauge | counter.
@@ -36,9 +38,14 @@ set_metric(Type, Name, Value, Labels) ->
-spec log_observation(counter, atom() | binary(), [atom() | binary()]) -> ok.
+-ifdef(NOTEST).
log_observation(counter, Name, Labels) ->
prometheus_counter:inc(Name, Labels),
ok.
+-else.
+log_observation(counter, _Name, _Labels) ->
+ ok.
+-endif.
-spec remove_metric(metric_type(), atom() | binary()) -> ok.
remove_metric(Type, Name) ->
@@ -56,91 +63,164 @@ format(prometheus) ->
update_internal_metrics(), %% TODO: Avoid too much calling here
prometheus_text_format:format().
-update_internal_metrics() ->
- %% Services
- Services = [ automate_storage_sup
+-spec get_internal_metrics() -> {ok, #internal_metrics{}, [iolist()]}.
+get_internal_metrics() ->
+ Services = [ {automate_storage, automate_storage}
- , automate_channel_engine
- , automate_channel_engine_sup
+ , {automate_channel_engine, automate_channel_engine_sup}
- , automate_rest_api_sup
+ , {automate_rest_api, automate_rest_api_sup}
- , automate_service_registry_sup
+ , {automate_service_registry, automate_service_registry_sup}
- , automate_bot_engine_runner_sup
- , automate_bot_engine_thread_runner_sup
- , automate_bot_engine_sup
+ , {automate_bot_engine_program_runner, automate_bot_engine_runner_sup}
+ , {automate_bot_engine_thread_runner, automate_bot_engine_thread_runner_sup}
+ , {automate_bot_engine, automate_bot_engine_sup}
- , automate_monitor_engine_runner_sup
- , automate_monitor_engine_sup
+ , {automate_monitor_engine_runner, automate_monitor_engine_runner_sup}
+ , {automate_monitor_engine, automate_monitor_engine_sup}
- , automate_service_port_engine_sup
+ , {automate_service_port_engine, automate_service_port_engine_sup}
],
- lists:foreach(fun (S) ->
- set_metric(boolean, automate_service,
- whereis(S) =/= undefined, [S])
- end, Services),
+ ServiceCounts = maps:from_list(lists:map(fun ({Name, Service}) ->
+ {Name, whereis(Service) =/= undefined}
+ end, Services)),
+
+ Errors = [],
%% Bots
- try
- supervisor:count_children(automate_bot_engine_runner_sup)
- of Bots ->
- set_metric(gauge, automate_bot_count,
- proplists:get_value(workers, Bots), [total]),
-
- set_metric(gauge, automate_bot_count,
- proplists:get_value(active, Bots), [running])
- catch BotErrNS:BotErr:BotStackTrace ->
- io:fwrite("Error counting bots: ~p~n", [{BotErrNS, BotErr, BotStackTrace}]),
- set_metric(gauge, automate_bot_count, 0, [running])
- end,
+ {BotCount, Err1} = try supervisor:count_children(automate_bot_engine_runner_sup)
+ of Bots ->
+ { maps:from_list(lists:filter(fun({K, _}) -> (K =:= active) or (K =:= workers) end, Bots))
+ , Errors}
+ catch BotErrNS:BotErr:BotStackTrace ->
+ { #{ active => undefined, worker => undefined }
+ , [{bot_engine_programs, {BotErrNS, BotErr, BotStackTrace}} | Errors]}
+ end,
%% Threads
- try
- supervisor:count_children(automate_bot_engine_thread_runner_sup)
- of Threads ->
- set_metric(gauge, automate_program_thread_count,
- proplists:get_value(workers, Threads), [total]),
-
- set_metric(gauge, automate_program_thread_count,
- proplists:get_value(active, Threads), [running])
- catch ThreadErrNS:ThreadErr:ThreadStackTrace ->
- io:fwrite("Error counting threads: ~p~n", [{ThreadErrNS, ThreadErr, ThreadStackTrace}]),
- set_metric(gauge, automate_program_thread_count, 0, [running])
- end,
+ { ThreadCount, Err2 } = try supervisor:count_children(automate_bot_engine_thread_runner_sup)
+ of Threads ->
+ { maps:from_list(lists:filter(fun({K, _}) -> (K =:= active) or (K =:= workers) end, Threads))
+ , Err1}
+ catch ThreadErrNS:ThreadErr:ThreadStackTrace ->
+ { #{ active => undefined, worker => undefined }
+ , [{bot_engine_threads, {ThreadErrNS, ThreadErr, ThreadStackTrace}} | Err1]}
+ end,
%% Monitors
- try
- supervisor:count_children(automate_monitor_engine_runner_sup)
- of Monitors ->
- set_metric(gauge, automate_monitor_count,
- proplists:get_value(workers, Monitors), [total]),
-
- set_metric(gauge, automate_monitor_count,
- proplists:get_value(active, Monitors), [running])
- catch MonitorErrNS:MonitorErr:MonitorStackTrace ->
- io:fwrite("Error counting monitors: ~p~n", [{MonitorErrNS, MonitorErr, MonitorStackTrace}]),
- set_metric(gauge, automate_monitor_count, 0, [running])
- end,
+ { MonitorCount, Err3 } = try supervisor:count_children(automate_monitor_engine_runner_sup)
+ of Monitors ->
+ { maps:from_list(lists:filter(fun({K, _}) -> (K =:= active) or (K =:= workers) end, Monitors))
+ , Err2}
+ catch MonitorErrNS:MonitorErr:MonitorStackTrace ->
+ { #{ active => undefined, worker => undefined }
+ , [{monitor_engine, {MonitorErrNS, MonitorErr, MonitorStackTrace}} | Err2]}
+ end,
%% Services
- case automate_service_registry:get_all_public_services() of
- {ok, PublicServices} ->
- set_metric(gauge, automate_service_count,
- maps:size(PublicServices), [public]),
-
- set_metric(gauge, automate_service_count,
- automate_service_registry:count_all_services(), [all]);
- {error, _, _} ->
- remove_metric(gauge, automate_service_count)
- end,
+ { ServiceCount, Err4 } = case automate_service_registry:get_all_public_services() of
+ {ok, PublicServices} ->
+ { #{ public => maps:size(PublicServices)
+ , all => automate_service_registry:count_all_services()
+ }
+ , Err3 }
+ end,
%% Users
{ ok
, UserCount, RegisteredUsersLastDay, RegisteredUsersLastWeek, RegisteredUsersLastMonth
, LoggedUsersLastHour, LoggedUsersLastDay, LoggedUsersLastWeek, LoggedUsersLastMonth
} = automate_storage_stats:get_user_metrics(),
+
+ { ok
+ , GroupCount, CreatedGroupsLastDay, CreatedGroupsLastWeek, CreatedGroupsLastMonth
+ } = automate_storage_stats:get_group_metrics(),
+
+ { ok
+ , NumBridgesPublic, NumBridgesPrivate
+ , NumConnections, NumUniqueConnections
+ , NumMessagesOnFlight
+ } = automate_service_port_engine_stats:get_bridge_metrics(),
+
+ {ok
+ , #internal_metrics{ services_active=ServiceCounts
+ , bot_count=BotCount
+ , thread_count=ThreadCount
+ , monitor_count=MonitorCount
+ , service_count=ServiceCount
+ , user_stats=#user_stat_metrics{ count=UserCount
+ , registered_last_day=RegisteredUsersLastDay
+ , registered_last_week=RegisteredUsersLastWeek
+ , registered_last_month=RegisteredUsersLastMonth
+ , logged_last_hour=LoggedUsersLastHour
+ , logged_last_day=LoggedUsersLastDay
+ , logged_last_week=LoggedUsersLastWeek
+ , logged_last_month=LoggedUsersLastMonth
+ }
+ , group_stats=#group_stat_metrics{ count=GroupCount
+ , created_last_day=CreatedGroupsLastDay
+ , created_last_week=CreatedGroupsLastWeek
+ , created_last_month=CreatedGroupsLastMonth
+ }
+ , bridge_stats=#bridge_stat_metrics{ public_count=NumBridgesPublic
+ , private_count=NumBridgesPrivate
+ , connections=NumConnections
+ , unique_connections=NumUniqueConnections
+ , messages_on_flight=NumMessagesOnFlight
+ }
+ }
+ , Err4}.
+
+%%====================================================================
+%% Functions for internal usage
+%%====================================================================
+update_internal_metrics() ->
+ %% Services
+ {ok, #internal_metrics{ services_active=Services
+ , bot_count=BotCount
+ , thread_count=ThreadCount
+ , monitor_count=MonitorCount
+ , service_count=ServiceCount
+ , user_stats=UserStats
+ , group_stats=GroupStats
+ , bridge_stats=BridgeStats
+ }, Errors} = get_internal_metrics(),
+ maps:map(fun(Service, Active) ->
+ set_metric(boolean, automate_service, Active, [Service])
+ end, Services),
+
+ maps:map(fun(Category, Count) ->
+ set_metric(gauge, automate_bot_count, Count, [Category])
+ end, BotCount),
+
+ maps:map(fun(Category, Count) ->
+ set_metric(gauge, automate_program_thread_count, Count, [Category])
+ end, ThreadCount),
+
+ maps:map(fun(Category, Count) ->
+ set_metric(gauge, automate_monitor_count, Count, [Category])
+ end, MonitorCount),
+
+ maps:map(fun(Category, Count) ->
+ set_metric(gauge, automate_service_count, Count, [Category])
+ end, ServiceCount),
+
+ set_metric(gauge, automate_node_count, length(nodes()) + 1, []),
+ set_metric(gauge, automate_mnesia_node_count, length(mnesia:system_info(db_nodes)), [all]),
+ set_metric(gauge, automate_mnesia_node_count, length(mnesia:system_info(running_db_nodes)), [active]),
+
+ %% Users
+ #user_stat_metrics{ count=UserCount
+ , registered_last_day=RegisteredUsersLastDay
+ , registered_last_week=RegisteredUsersLastWeek
+ , registered_last_month=RegisteredUsersLastMonth
+ , logged_last_hour=LoggedUsersLastHour
+ , logged_last_day=LoggedUsersLastDay
+ , logged_last_week=LoggedUsersLastWeek
+ , logged_last_month=LoggedUsersLastMonth
+ } = UserStats,
set_metric(gauge, automate_user_count, UserCount, [registered]),
set_metric(gauge, automate_registered_users_last_day, RegisteredUsersLastDay, [registered]),
set_metric(gauge, automate_registered_users_last_week, RegisteredUsersLastWeek, [registered]),
@@ -150,19 +230,84 @@ update_internal_metrics() ->
set_metric(gauge, automate_logged_users_last_day, LoggedUsersLastDay, [registered]),
set_metric(gauge, automate_logged_users_last_week, LoggedUsersLastWeek, [registered]),
set_metric(gauge, automate_logged_users_last_month, LoggedUsersLastMonth, [registered]),
+
+ %% Groups
+ #group_stat_metrics{ count=GroupCount
+ , created_last_day=CreatedGroupsLastDay
+ , created_last_week=CreatedGroupsLastWeek
+ , created_last_month=CreatedGroupsLastMonth
+ } = GroupStats,
+ set_metric(gauge, automate_group_count, GroupCount, [created]),
+ set_metric(gauge, automate_created_groups_last_day, CreatedGroupsLastDay, [created]),
+ set_metric(gauge, automate_created_groups_last_week, CreatedGroupsLastWeek, [created]),
+ set_metric(gauge, automate_created_groups_last_month, CreatedGroupsLastMonth, [created]),
+
+ %% Bridges
+ #bridge_stat_metrics{ public_count=NumBridgesPublic
+ , private_count=NumBridgesPrivate
+ , connections=NumConnections
+ , unique_connections=NumUniqueConnections
+ , messages_on_flight=NumMessagesOnFlight
+ } = BridgeStats,
+ set_metric(gauge, automate_bridges_count, NumBridgesPublic, [public]),
+ set_metric(gauge, automate_bridges_count, NumBridgesPrivate, [private]),
+ set_metric(gauge, automate_bridges_connections_count, NumConnections, []),
+ set_metric(gauge, automate_bridges_unique_connections_count, NumUniqueConnections, []),
+ set_metric(gauge, automate_bridges_messages_on_flight_count, NumMessagesOnFlight, []),
+
+ %% Program logs
+ try
+ automate_storage_stats:get_program_metrics()
+ of
+ {ok, LogCountPerProgram} ->
+ ok = set_log_count_metrics(LogCountPerProgram)
+ catch ErrNS:Err:_ ->
+ automate_logging:log_platform(warning, io_lib:format("Error getting user program logs. Reason: ~p",
+ [{ErrNS, Err}]))
+ end,
+
+ lists:map(fun({Module, Reason}) ->
+ case Reason of
+ {ErrorNS, Error, StackTrace} ->
+ automate_logging:log_platform(warning, ErrorNS, Error, StackTrace);
+ _ ->
+ automate_logging:log_platform(warning, io_lib:format("Error getting stats for ~p. Reason: ~p",
+ [Module, Reason]))
+ end
+ end, Errors),
+
+ ok.
+
+set_log_count_metrics(LogCountPerProgram) ->
+ %% No foreach, so we use maps:map/2
+ maps:map(fun(ProgramId, Value) ->
+ maps:map(
+ fun(Severity, SubCategories) ->
+ maps:map(
+ fun(Category, Count) ->
+ set_metric(gauge, automate_program_log_count, Count, [ProgramId, Severity, Category])
+ end, SubCategories)
+ end, Value),
+ ok
+ end, LogCountPerProgram),
ok.
-%%====================================================================
-%% Functions for internal usage
-%%====================================================================
prepare() ->
add_metric(boolean, automate_service, <<"State of automate service.">>, [name]),
+ add_metric(gauge, automate_node_count, <<"Automate's backend cluster node count.">>, []),
+ add_metric(gauge, automate_mnesia_node_count, <<"Automate's mnesia node count.">>, [state]),
add_metric(gauge, automate_bot_count, <<"Automate's bot.">>, [state]),
add_metric(gauge, automate_program_thread_count, <<"Automate's program thread count.">>, [state]),
add_metric(gauge, automate_monitor_count, <<"Automate's monitor.">>, [state]),
add_metric(gauge, automate_service_count, <<"Automate's services.">>, [visibility]),
+ add_metric(gauge, automate_program_log_count, <<"Logs generated by a program.">>, [program, severity, log_type]),
+
+ add_metric(gauge, automate_bridges_count, <<"Number of bridges existing on the platform.">>, [visibility]),
+ add_metric(gauge, automate_bridges_connections_count, <<"Number of bridge connections established to the platform.">>, []),
+ add_metric(gauge, automate_bridges_unique_connections_count, <<"Number of bridges which have at least one established connection to the platform.">>, []),
+ add_metric(gauge, automate_bridges_messages_on_flight_count, <<"Number of messages on flight to bridges.">>, []),
add_metric(gauge, automate_user_count, <<"Automate's user.">>, [state]),
add_metric(gauge, automate_registered_users_last_day, <<"Users registered in the last 24 hours.">>, [state]),
@@ -173,6 +318,12 @@ prepare() ->
add_metric(gauge, automate_logged_users_last_day, <<"Users logged in the last 24 hours.">>, [state]),
add_metric(gauge, automate_logged_users_last_week, <<"Users logged in the last 7 days.">>, [state]),
add_metric(gauge, automate_logged_users_last_month, <<"Users logged in the last 28 days.">>, [state]),
+
+ add_metric(gauge, automate_group_count, <<"Automate's groups.">>, [state]),
+ add_metric(gauge, automate_created_groups_last_day, <<"Groups created in the last 24 hours.">>, [state]),
+ add_metric(gauge, automate_created_groups_last_week, <<"Groups created in the last 7 days.">>, [state]),
+ add_metric(gauge, automate_created_groups_last_month, <<"Groups created in the last 28 days.">>, [state]),
+
ok.
diff --git a/backend/apps/automate_stats/src/automate_stats_app.erl b/backend/apps/automate_stats/src/automate_stats_app.erl
index 9649820f..563415a5 100644
--- a/backend/apps/automate_stats/src/automate_stats_app.erl
+++ b/backend/apps/automate_stats/src/automate_stats_app.erl
@@ -8,16 +8,18 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
-
-start(_StartType, _StartArgs) ->
+start() ->
automate_stats:prepare(),
automate_stats_sup:start_link().
+start(_StartType, _StartArgs) ->
+ start().
+
%%--------------------------------------------------------------------
stop(_State) ->
ok.
diff --git a/backend/apps/automate_stats/src/records.hrl b/backend/apps/automate_stats/src/records.hrl
new file mode 100644
index 00000000..50f25108
--- /dev/null
+++ b/backend/apps/automate_stats/src/records.hrl
@@ -0,0 +1,32 @@
+-record(user_stat_metrics, { count :: pos_integer()
+ , registered_last_day :: pos_integer()
+ , registered_last_week :: pos_integer()
+ , registered_last_month :: pos_integer()
+ , logged_last_hour :: pos_integer()
+ , logged_last_day :: pos_integer()
+ , logged_last_week :: pos_integer()
+ , logged_last_month :: pos_integer()
+ }).
+
+-record(group_stat_metrics, { count :: pos_integer()
+ , created_last_day :: pos_integer()
+ , created_last_week :: pos_integer()
+ , created_last_month :: pos_integer()
+ }).
+
+-record(bridge_stat_metrics, { public_count :: pos_integer()
+ , private_count :: pos_integer()
+ , connections :: pos_integer()
+ , unique_connections :: pos_integer()
+ , messages_on_flight :: pos_integer()
+ }).
+
+-record(internal_metrics, { services_active :: map()
+ , bot_count :: #{ active => number(), workers => number()}
+ , thread_count :: #{ active => number(), workers => number()}
+ , monitor_count :: #{ active => number(), workers => number()}
+ , service_count :: #{ public => number(), all => number()}
+ , user_stats :: #user_stat_metrics{}
+ , group_stats :: #group_stat_metrics{}
+ , bridge_stats :: #bridge_stat_metrics{}
+ }).
diff --git a/backend/apps/automate_storage/src/automate_storage.app.src b/backend/apps/automate_storage/src/automate_storage.app.src
index 2ca248ea..a070d937 100644
--- a/backend/apps/automate_storage/src/automate_storage.app.src
+++ b/backend/apps/automate_storage/src/automate_storage.app.src
@@ -8,7 +8,7 @@
, automate_configuration
, mnesia
, uuid
- , libsodium
+ , eargon2
]},
{env, [
]},
diff --git a/backend/apps/automate_storage/src/automate_storage.erl b/backend/apps/automate_storage/src/automate_storage.erl
index 39ebf1b2..c8727f9d 100644
--- a/backend/apps/automate_storage/src/automate_storage.erl
+++ b/backend/apps/automate_storage/src/automate_storage.erl
@@ -4,15 +4,24 @@
-export([ create_user/4
, login_user/2
, get_user/1
- , generate_token_for_user/1
+ , generate_token_for_user/3
, delete_user/1
- , get_session_username/2
- , get_session_userid/2
+ , check_session_username/3
+ , check_session_userid/3
, create_monitor/2
, get_monitor_from_id/1
, dirty_list_monitors/0
, lists_monitors_from_username/1
+ , list_monitors/1
, get_userid_from_username/1
+ , update_user_settings/3
+ , set_owner_public_listings/2
+ , get_owner_public_listings/1
+ , list_public_collaborators/1
+ , promote_user_to_admin/1
+ , admin_list_users/0
+ , set_user_in_preview/2
+ , search_users/1
, create_mail_verification_entry/1
, verify_registration_with_code/1
@@ -22,50 +31,105 @@
, reset_password/2
, create_program/2
+ , create_program/3
, get_program/2
, lists_programs_from_username/1
- , list_programs_from_userid/1
+ , list_programs/1
, update_program/3
+ , fix_program_channel/1
+
+ , checkpoint_program/3
+ , get_last_checkpoint_content/1
+
+ , update_program_by_id/2
, update_program_metadata/3
+ , update_program_metadata/2
, delete_program/2
+ , delete_program/1
, delete_running_process/1
- , update_program_status/3
+ , update_program_status/2
+ , is_user_allowed/3
+ , get_program_pages/1
+ , get_program_page/2
+ , add_user_asset/3
+ , get_user_asset_info/2
+ , list_user_assets/1
, get_program_owner/1
, get_program_pid/1
+ , get_program_variables/1
, get_user_from_pid/1
, register_program_runner/2
, get_program_from_id/1
, register_program_tags/2
, get_tags_program_from_id/1
+ , get_logs_from_program_id/1
, dirty_list_running_programs/0
+ , store_program_event/2
+ , get_program_events/1
+
+ , add_user_generated_log/1
+ , get_user_generated_logs/1
, create_thread/2
, dirty_list_running_threads/0
, register_thread_runner/2
, get_thread_from_id/1
+ , dirty_is_thread_alive/1
, delete_thread/1
, update_thread/1
, get_threads_from_program/1
, set_program_variable/3
+ , delete_program_variable/2
, get_program_variable/2
+ , set_widget_value/3
+ , get_widget_values_in_program/1
+
+ , log_program_error/1
+ , mark_successful_call_to_bridge/2
+ , mark_failed_call_to_bridge/2
, create_custom_signal/2
- , list_custom_signals_from_user_id/1
+ , list_custom_signals/1
+
+ , create_group/3
+ , delete_group/1
+ , update_group_metadata/2
+ , get_user_groups/1
+ , get_group_by_name/1
+ , is_allowed_to_read_in_group/2
+ , is_allowed_to_write_in_group/2
+ , is_allowed_to_admin_in_group/2
+ , can_user_admin_as/2
+ , can_user_edit_as/2
+ , can_user_view_as/2
+ , list_collaborators/1
+ , add_collaborators/2
+ , update_collaborators/2
+
+ , is_user_allowed_to_create_public_bridges/1
+ , is_user_allowed_to_connect_to_bridges_in_group/2
, add_mnesia_node/1
, register_table/2
+
+ %% Utils
+ , wrap_transaction/1
]).
-export([start_link/0]).
-define(SERVER, ?MODULE).
+-include("./security_params.hrl").
-include("./databases.hrl").
-include("./records.hrl").
--include("../automate_bot_engine/src/program_records.hrl").
+-include("../../automate_bot_engine/src/program_records.hrl").
+-include_lib("./_build/default/lib/eargon2/include/eargon2.hrl").
--define(DEFAULT_PROGRAM_TYPE, scratch_program).
-define(WAIT_READY_LOOP_TIME, 1000).
+-define(DEFAULT_PROGRAM_TYPE, scratch_program).
+-define(ETS_TABLE_SECONDARY_NODE_RESTART, autoamte_storage_secondary_node_ets_table_for_restart).
+
%%====================================================================
%% API functions
%%====================================================================
@@ -76,12 +140,19 @@ create_user(Username, Password, Email, Status) ->
undefined -> undefined;
_ -> cipher_password(Password)
end,
+
+ CanonicalUsername = automate_storage_utils:canonicalize(Username),
+
RegisteredUserData = #registered_user_entry{ id=UserId
, username=Username
+ , canonical_username=CanonicalUsername
, password=CipheredPassword
, email=Email
, registration_time=CurrentTime
, status=Status
+ , is_admin=false
+ , is_advanced=false
+ , is_in_preview=false
},
case save_unique_user(RegisteredUserData) of
ok ->
@@ -99,31 +170,38 @@ delete_user(UserId) ->
Result.
login_user(Username, Password) ->
- case get_userid_and_password_from_username(Username) of
+ Result = case binary:match(Username, <<"@">>) of
+ nomatch ->
+ get_user_from_username(Username);
+ _ ->
+ get_user_from_email(Username)
+ end,
+ case Result of
{ok, #registered_user_entry{ id=UserId
, password=StoredPassword
, status=Status
}} ->
- case libsodium_crypto_pwhash:str_verify(StoredPassword, Password) =:= 0 of
- true ->
+ case verify_passwd_hash(StoredPassword, Password) of
+ ok ->
case Status of
ready ->
SessionToken = generate_id(),
- ok = add_token_to_user(UserId, SessionToken),
+ ok = add_token_to_user(UserId, SessionToken, all, session),
{ ok, {SessionToken, UserId} };
_ ->
{error, {user_not_ready, Status}}
end;
- _ ->
+ {error, ?EARGON2_ERROR_CODE_VERIFY_MISMATCH} ->
+ {error, invalid_user_password};
+ {error, ErrorCode} ->
+ automate_logging:log_platform(error, io_lib:format("Error verifying user password (id=~p), error code: ~p", [UserId, ErrorCode])),
{error, invalid_user_password}
end;
{error, no_user_found} ->
- {error, no_user_found};
-
- {error, Reason} ->
- { error, Reason }
+ {error, no_user_found}
end.
+-spec get_user(binary()) -> {ok, #registered_user_entry{}} | {error, not_found}.
get_user(UserId) ->
Transaction = fun() ->
case mnesia:read(?REGISTERED_USERS_TABLE, UserId) of
@@ -133,6 +211,37 @@ get_user(UserId) ->
{error, not_found}
end
end,
+ mnesia:ets(Transaction).
+
+promote_user_to_admin(UserId) ->
+ Transaction = fun() ->
+ case mnesia:read(?REGISTERED_USERS_TABLE, UserId) of
+ [User] ->
+ ok = mnesia:write(?REGISTERED_USERS_TABLE
+ , User#registered_user_entry{ is_admin=true }
+ , write);
+ [] ->
+ {error, not_found}
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ {error, Reason}
+ end.
+
+set_user_in_preview(UserId, InPreview) when is_boolean(InPreview) ->
+ Transaction = fun() ->
+ case mnesia:read(?REGISTERED_USERS_TABLE, UserId) of
+ [User] ->
+ ok = mnesia:write(?REGISTERED_USERS_TABLE
+ , User#registered_user_entry{ is_in_preview=InPreview }
+ , write);
+ [] ->
+ {error, not_found}
+ end
+ end,
case mnesia:transaction(Transaction) of
{atomic, Result} ->
Result;
@@ -140,11 +249,41 @@ get_user(UserId) ->
{error, Reason}
end.
-generate_token_for_user(UserId) ->
+search_users(Query) ->
+ {ok, QueryRe} = re:compile(query_to_re(Query)),
+ Transaction = fun() ->
+ {ok, search_users_iter(mnesia:first(?REGISTERED_USERS_TABLE), [], QueryRe)}
+ end,
+ mnesia:activity(ets, Transaction).
+
+admin_list_users() ->
+ Transaction = fun() ->
+ Result = lists:map(fun(UserId) ->
+ %% Pull last usage time of a user
+ [V] = mnesia:read(?REGISTERED_USERS_TABLE, UserId),
+ Sessions = get_userid_sessions(UserId),
+ Last = case Sessions of
+ [] -> undefined;
+ _ ->
+ Times = lists:map(fun(#user_session_entry{
+ session_last_used_time=LastTime }) ->
+ LastTime
+ end, Sessions),
+ lists:last(lists:sort(Times))
+ end,
+ {V, Last}
+ end,
+ mnesia:all_keys(?REGISTERED_USERS_TABLE)),
+ { ok, Result }
+ end,
+ mnesia:ets(Transaction).
+
+-spec generate_token_for_user(binary(), session_scope(), session_expiration_time()) -> {ok, binary()} | {error, user_not_ready} | {error, _}.
+generate_token_for_user(UserId, Scope, Expiration) ->
case get_user(UserId) of
{ok, #registered_user_entry{ status=ready }} ->
SessionToken = generate_id(),
- ok = add_token_to_user(UserId, SessionToken),
+ ok = add_token_to_user(UserId, SessionToken, Scope, Expiration),
{ ok, SessionToken };
{ok, _} ->
{error, user_not_ready};
@@ -152,27 +291,33 @@ generate_token_for_user(UserId) ->
{error, Reason}
end.
-get_session_username(SessionId, RefreshUsedTime) when is_binary(SessionId) ->
+-spec check_session_username(binary(), session_scope_item(), boolean()) -> { ok, binary() }| {error, session_not_found | scope_not_allowed}.
+check_session_username(SessionId, Scope, RefreshUsedTime) when is_binary(SessionId) ->
Transaction = fun() ->
case mnesia:read(?USER_SESSIONS_TABLE, SessionId) of
[] ->
{ error, session_not_found };
- [Session=#user_session_entry{ user_id=UserId } | _] ->
- case mnesia:read(?REGISTERED_USERS_TABLE, UserId) of
- [] ->
- %% TODO log event, this shouldn't happen
- { error, session_not_found };
- [#registered_user_entry{username=Username} | _] ->
- ok = case RefreshUsedTime of
- true ->
- mnesia:write(
- ?USER_SESSIONS_TABLE
- , Session#user_session_entry{session_last_used_time=erlang:system_time(second)}
- , write);
- false ->
- ok
- end,
- {ok, Username}
+ [Session=#user_session_entry{ user_id=UserId, session_scope=TokenScope } | _] ->
+ case token_scope_covers(TokenScope, Scope) of
+ true ->
+ case mnesia:read(?REGISTERED_USERS_TABLE, UserId) of
+ [] ->
+ %% TODO log event, this shouldn't happen
+ { error, session_not_found };
+ [#registered_user_entry{canonical_username=Username} | _] ->
+ ok = case RefreshUsedTime of
+ true ->
+ mnesia:write(
+ ?USER_SESSIONS_TABLE
+ , Session#user_session_entry{session_last_used_time=erlang:system_time(second)}
+ , write);
+ false ->
+ ok
+ end,
+ {ok, Username}
+ end;
+ false ->
+ {error, scope_not_allowed}
end
end
end,
@@ -180,22 +325,28 @@ get_session_username(SessionId, RefreshUsedTime) when is_binary(SessionId) ->
{atomic, Result} = mnesia:transaction(Transaction),
Result.
-get_session_userid(SessionId, RefreshUsedTime) when is_binary(SessionId) ->
+-spec check_session_userid(binary(), session_scope_item(), boolean()) -> { ok, binary() }| {error, session_not_found | scope_not_allowed}.
+check_session_userid(SessionId, Scope, RefreshUsedTime) when is_binary(SessionId) ->
Transaction = fun() ->
case mnesia:read(?USER_SESSIONS_TABLE, SessionId) of
[] ->
{ error, session_not_found };
- [Session=#user_session_entry{ user_id=UserId } | _] ->
- ok = case RefreshUsedTime of
- true ->
- mnesia:write(
- ?USER_SESSIONS_TABLE
- , Session#user_session_entry{session_last_used_time=erlang:system_time(second)}
- , write);
- false ->
- ok
- end,
- {ok, UserId}
+ [Session=#user_session_entry{ user_id=UserId, session_scope=TokenScope } | _] ->
+ case token_scope_covers(TokenScope, Scope) of
+ true ->
+ ok = case RefreshUsedTime of
+ true ->
+ mnesia:write(
+ ?USER_SESSIONS_TABLE
+ , Session#user_session_entry{session_last_used_time=erlang:system_time(second)}
+ , write);
+ false ->
+ ok
+ end,
+ {ok, UserId};
+ false ->
+ {error, scope_not_allowed}
+ end
end
end,
@@ -203,10 +354,12 @@ get_session_userid(SessionId, RefreshUsedTime) when is_binary(SessionId) ->
Result.
-spec create_monitor(binary(), #monitor_entry{}) -> {ok, binary()} | {error, any()}.
-create_monitor(Username, MonitorDescriptor=#monitor_entry{ id=none, user_id=none }) ->
- {ok, UserId} = get_userid_from_username(Username),
+create_monitor(Username, MonitorDescriptor=#monitor_entry{ id=none, owner=none }) ->
+ io:fwrite("\033[7m[create_monitor(Username,...)] To be deprecated\033[0m~n"),
+
+ {ok, Owner} = get_userid_from_username(Username),
MonitorId = generate_id(),
- Monitor = MonitorDescriptor#monitor_entry{ id=MonitorId, user_id=UserId },
+ Monitor = MonitorDescriptor#monitor_entry{ id=MonitorId, owner=Owner },
case store_new_monitor(Monitor) of
ok ->
{ ok, MonitorId };
@@ -223,16 +376,16 @@ get_monitor_from_id(MonitorId) ->
Transaction = fun() ->
mnesia:read(?USER_MONITORS_TABLE, MonitorId)
end,
- case mnesia:transaction(Transaction) of
- { atomic, [Result] } ->
+ case mnesia:ets(Transaction) of
+ [Result] ->
Result;
- { aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
- {error, mnesia:error_description(Reason)}
+ [] ->
+ {error, not_found}
end.
-spec lists_monitors_from_username(binary()) -> {'ok', [ { binary(), binary() } ] }.
lists_monitors_from_username(Username) ->
+ io:fwrite("\033[7m[lists_monitors_from_username] To be deprecated\033[0m~n"),
case retrieve_monitors_list_from_username(Username) of
{ok, Monitors} ->
{ ok
@@ -241,6 +394,14 @@ lists_monitors_from_username(Username) ->
X
end.
+-spec list_monitors(owner_id()) -> {'ok', [ #monitor_entry{} ] }.
+list_monitors(Owner) ->
+ Transaction = fun() ->
+ {ok, mnesia:index_read(?USER_MONITORS_TABLE, Owner, #monitor_entry.owner)}
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+
-spec create_mail_verification_entry(binary()) -> {ok, binary()} | {error, _}.
create_mail_verification_entry(UserId) ->
create_verification_entry(UserId, registration_mail_verification).
@@ -251,19 +412,27 @@ verify_registration_with_code(RegistrationCode) ->
case mnesia:read(?USER_VERIFICATION_TABLE, RegistrationCode) of
[] ->
{error, not_found};
- [#user_verification_entry{ user_id=UserId
- , verification_type=registration_mail_verification
- }] ->
+ [Verification=#user_verification_entry{ user_id=UserId
+ , verification_type=registration_mail_verification
+ , used=false
+ }] ->
case mnesia:read(?REGISTERED_USERS_TABLE, UserId) of
[] ->
{error, user_not_found};
[User=#registered_user_entry{status=mail_not_verified}] ->
ok = mnesia:write(?REGISTERED_USERS_TABLE, User#registered_user_entry{ status=ready }, write),
- ok = mnesia:delete({?USER_VERIFICATION_TABLE, RegistrationCode}),
+ ok = mnesia:write(?USER_VERIFICATION_TABLE, Verification#user_verification_entry{ used=true }, write),
{ok, UserId};
[#registered_user_entry{status=Status}] ->
{error, {status_mismatch, Status}}
end;
+
+ [#user_verification_entry{ user_id=UserId
+ , verification_type=registration_mail_verification
+ , used=true
+ }] ->
+ {ok, UserId};
+
[#user_verification_entry{ verification_type=VerificationType }] ->
{error, {invalid_verification_type, VerificationType}}
end
@@ -280,7 +449,7 @@ verify_registration_with_code(RegistrationCode) ->
{ atomic, Result } ->
Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, Reason}
end.
@@ -290,10 +459,14 @@ create_recovery_verification(UserId) ->
get_user_from_mail_address(Email) ->
MatchHead = #registered_user_entry{ id='_'
, username='_'
+ , canonical_username='_'
, password='_'
, email='$1'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
Guard = {'==', '$1', Email},
ResultColumn = '$_',
@@ -307,12 +480,7 @@ get_user_from_mail_address(Email) ->
{error, no_user_found}
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- {error, Reason}
- end.
+ mnesia:ets(Transaction).
-spec reset_password(binary(), binary()) -> ok | {error, _}.
reset_password(VerificationCode, Password) ->
@@ -329,6 +497,8 @@ reset_password(VerificationCode, Password) ->
ok = mnesia:write(?REGISTERED_USERS_TABLE,
User#registered_user_entry{ password=HashedPassword },
write),
+
+ %% Cannot be used more than once
ok = mnesia:delete({?USER_VERIFICATION_TABLE, VerificationCode})
end;
[#user_verification_entry{ verification_type=OtherVerificationType }] ->
@@ -343,7 +513,7 @@ reset_password(VerificationCode, Password) ->
{ atomic, Result } ->
Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, Reason}
end.
@@ -351,16 +521,31 @@ reset_password(VerificationCode, Password) ->
check_password_reset_verification_code(VerificationCode) ->
check_verification_code(VerificationCode, password_reset_verification).
-create_program(Username, ProgramName) ->
- {ok, UserId} = get_userid_from_username(Username),
+create_program(User, ProgramName) ->
+ create_program(User, ProgramName, ?DEFAULT_PROGRAM_TYPE).
+
+create_program(Username, ProgramName, ProgramType) when is_binary(Username) ->
+ io:fwrite("\033[7m[create_program(Username,...)] To be deprecated\033[0m~n"),
+ {ok, Owner} = get_userid_from_username(Username),
+ create_program(Owner, ProgramName, ProgramType);
+
+create_program(Owner, ProgramName, ProgramType) ->
ProgramId = generate_id(),
+ {ok, ProgramChannel} = automate_channel_engine:create_channel(),
+ CurrentTime = erlang:system_time(second),
UserProgram = #user_program_entry{ id=ProgramId
- , user_id=UserId
+ , owner=Owner
, program_name=ProgramName
- , program_type=?DEFAULT_PROGRAM_TYPE
+ , program_type=ProgramType
, program_parsed=undefined
, program_orig=undefined
, enabled=true
+ , program_channel=ProgramChannel
+ , creation_time=CurrentTime
+ , last_upload_time=0
+ , last_successful_call_time=0
+ , last_failed_call_time=0
+ , visibility=private
},
case store_new_program(UserProgram) of
ok ->
@@ -369,32 +554,32 @@ create_program(Username, ProgramName) ->
{ error, Reason }
end.
-
get_program(Username, ProgramName) ->
retrieve_program(Username, ProgramName).
--spec lists_programs_from_username(binary()) -> {'ok', [ { binary(), binary(), boolean() } ] }.
+-spec lists_programs_from_username(binary()) -> {'ok', [ #user_program_entry{} ] }.
lists_programs_from_username(Username) ->
+ io:fwrite("\033[7m[lists_programs_from_username] To be deprecated\033[0m~n"),
case retrieve_program_list_from_username(Username) of
{ok, Programs} ->
{ ok
- , [{Id, Name, Enable} || [#user_program_entry{id=Id, program_name=Name, enabled=Enable}] <- Programs]};
+ , lists:map(fun ([E]) -> E end, Programs)
+ };
X ->
X
end.
-list_programs_from_userid(Userid) ->
- case retrieve_program_list_from_userid(Userid) of
- {ok, Programs} ->
- { ok
- , [{Id, Name, Enabled} || [#user_program_entry{id=Id, program_name=Name, enabled=Enabled}] <- Programs]};
- X ->
- X
- end.
--spec update_program_status(binary(), binary(), boolean()) -> 'ok' | { 'error', any() }.
-update_program_status(Username, ProgramId, Status)->
+-spec list_programs(owner_id()) -> {ok, [#user_program_entry{}, ...]} | {error, any()}.
+list_programs(Owner) ->
+ Transaction = fun() ->
+ {ok, mnesia:index_read(?USER_PROGRAMS_TABLE, Owner, #user_program_entry.owner)}
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec update_program_status(binary(), boolean()) -> 'ok' | { 'error', any() }.
+update_program_status(ProgramId, Status)->
Transaction = fun() ->
case mnesia:read(?USER_PROGRAMS_TABLE, ProgramId) of
[Program] ->
@@ -404,42 +589,192 @@ update_program_status(Username, ProgramId, Status)->
end
end,
case mnesia:transaction(Transaction) of
- { atomic, Result } ->
+ { atomic, ok } ->
ok;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, mnesia:error_description(Reason)}
end.
+-spec is_user_allowed(owner_id(), binary(), read_program|edit_program|delete_program|admin_program) -> {ok, boolean()} | {error, any()}.
+is_user_allowed(Owner, ProgramId, Action) ->
+ Check = case Action of
+ read_program -> fun can_user_view_as/2;
+ edit_program -> fun can_user_edit_as/2;
+ delete_program -> fun can_user_edit_as/2;
+ admin_program -> fun can_user_admin_as/2
+ end,
+ Transaction = fun() ->
+ case {mnesia:read(?USER_PROGRAMS_TABLE, ProgramId), Action} of
+ {[#user_program_entry{visibility=public}], read_program} ->
+ {ok, true};
+ {[#user_program_entry{visibility=shareable}], read_program} ->
+ {ok, true};
+ {[#user_program_entry{owner=RealOwner}], _} ->
+ {ok, Check(Owner, RealOwner)};
+ {[], _} ->
+ {error, not_found}
+ end
+ end,
+ mnesia:ets(Transaction).
+
+-spec get_program_pages(ProgramId :: binary()) -> {ok, [#program_pages_entry{}]} | {error, not_found}.
+get_program_pages(ProgramId) ->
+ T = fun() ->
+ {ok, mnesia:index_read(?PROGRAM_PAGES_TABLE, ProgramId, #program_pages_entry.program_id)}
+ end,
+ wrap_transaction(mnesia:ets(T)).
+
+-spec get_program_page(ProgramId :: binary(), Path :: binary()) -> {ok, #program_pages_entry{}} | {error, not_found}.
+get_program_page(ProgramId, Path) ->
+ T = fun() ->
+ case mnesia:read(?PROGRAM_PAGES_TABLE, {ProgramId, Path}) of
+ [ Page ] -> {ok, Page};
+ [] -> {error, not_found}
+ end
+ end,
+ wrap_transaction(mnesia:ets(T)).
+
+-spec add_user_asset(OwnerId :: owner_id(), AssetId :: binary(), MimeType :: mime_type()) -> ok.
+add_user_asset(OwnerId, AssetId, MimeType) ->
+ T = fun() ->
+ mnesia:write(?USER_ASSET_TABLE, #user_asset_entry{ asset_id={ OwnerId, AssetId }
+ , owner_id=OwnerId
+ , mime_type=MimeType
+ }, write)
+ end,
+ wrap_transaction(mnesia:transaction(T)).
+
+-spec get_user_asset_info(OwnerId :: owner_id(), AssetId :: binary()) -> { ok, #user_asset_entry{} } | {error, not_found}.
+get_user_asset_info(OwnerId, AssetId) ->
+ T = fun() ->
+ case mnesia:read(?USER_ASSET_TABLE, { OwnerId, AssetId }) of
+ [] -> {error, not_found};
+ [Entry] -> {ok, Entry}
+ end
+ end,
+ wrap_transaction(mnesia:ets(T)).
+
+-spec list_user_assets(OwnerId :: owner_id()) -> { ok, [#user_asset_entry{}] }.
+list_user_assets(OwnerId) ->
+ T = fun() ->
+ {ok, mnesia:index_read(?USER_ASSET_TABLE, OwnerId, #user_asset_entry.owner_id)}
+ end,
+ wrap_transaction(mnesia:ets(T)).
+
+
-spec update_program(binary(), binary(), #stored_program_content{}) -> { 'ok', binary() } | { 'error', any() }.
update_program(Username, ProgramName, Content)->
store_new_program_content(Username, ProgramName, Content).
--spec update_program_metadata(binary(), binary(), #editable_user_program_metadata{}) -> { 'ok', binary() } | { 'error', any() }.
-update_program_metadata(Username, ProgramName, #editable_user_program_metadata{program_name=NewProgramName})->
+-spec fix_program_channel(binary()) -> ok | {error, nothing_to_fix | not_found}.
+fix_program_channel(ProgramId) ->
+ Transaction = fun() ->
+ case mnesia:read(?USER_PROGRAMS_TABLE, ProgramId) of
+ [] -> {error, not_found};
+ [Program=#user_program_entry{program_channel=ChannelId}] ->
+ case automate_channel_engine_mnesia_backend:exists_channel(ChannelId) of
+ true ->
+ {error, nothing_to_fix};
+ false ->
+ {ok, NewChannel} = automate_channel_engine:create_channel(),
+ ok = mnesia:write(?USER_PROGRAMS_TABLE, Program#user_program_entry{program_channel=NewChannel}, write)
+ end
+ end
+ end,
+ wrap_transaction(mnesia:transaction(Transaction)).
+
+-spec update_program_by_id(binary(), #stored_program_content{}) -> { 'ok', binary() } | { 'error', any() }.
+update_program_by_id(ProgramId, Content)->
+ store_new_program_content(ProgramId, Content).
+
+-spec update_program_metadata(binary(), binary(), map()) -> { 'ok', binary() } | { 'error', any() }.
+update_program_metadata(Username, ProgramName, MetadataChanges)->
case retrieve_program(Username, ProgramName) of
- {ok, ProgramEntry=#user_program_entry{id=ProgramId}} ->
- Transaction = fun() ->
- ok = mnesia:write(?USER_PROGRAMS_TABLE,
- ProgramEntry#user_program_entry{program_name=NewProgramName}, write),
- {ok, ProgramId}
- end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- io:format("Register result: ~p~n", [Result]),
- Result;
- { aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
- {error, mnesia:error_description(Reason)}
- end;
+ {ok, #user_program_entry{id=ProgramId}} ->
+ update_program_metadata(ProgramId, MetadataChanges);
X ->
X
end.
+-spec update_program_metadata(binary(), map()) -> { 'ok', binary() } | { 'error', any() }.
+update_program_metadata(ProgramId, MetadataChanges)->
+ Transaction = fun() ->
+ case get_program_from_id(ProgramId) of
+ {ok, ProgramEntry=#user_program_entry{id=ProgramId}} ->
+ C1 = case MetadataChanges of
+ #{ <<"name">> := NewProgramName } ->
+ ProgramEntry#user_program_entry{program_name=NewProgramName};
+ _ ->
+ ProgramEntry
+ end,
+ C2 = case MetadataChanges of
+ #{ <<"visibility">> := Visibility } ->
+ C1#user_program_entry{ visibility=parse_visibility(Visibility) };
+ _ ->
+ C1
+ end,
+ ok = mnesia:write(?USER_PROGRAMS_TABLE
+ , C2
+ , write),
+ {ok, ProgramId};
+ X ->
+ X
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ { atomic, Result } ->
+ io:format("Register result: ~p~n", [Result]),
+ Result;
+ { aborted, Reason } ->
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
+ {error, mnesia:error_description(Reason)}
+ end.
+
+-spec checkpoint_program(binary(), binary(), any()) -> 'ok' | { 'error', any() }.
+checkpoint_program(UserId, ProgramId, Content)->
+ CurrentTime = erlang:system_time(millisecond),
+ Transaction = fun() ->
+ ok = mnesia:write(?USER_PROGRAM_CHECKPOINTS_TABLE,
+ #user_program_checkpoint{ program_id=ProgramId
+ , user_id=UserId
+ , event_time=CurrentTime
+ , content=Content
+ }, write),
+ ok = mnesia:delete(?USER_PROGRAM_EVENTS_TABLE, ProgramId, write)
+ end,
+ case mnesia:transaction(Transaction) of
+ { atomic, Result } ->
+ Result;
+ { aborted, Reason } ->
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
+
+ {error, mnesia:error_description(Reason)}
+ end.
+
+-spec get_last_checkpoint_content(binary()) -> {ok, #user_program_checkpoint{}} | {error, any()}.
+get_last_checkpoint_content(ProgramId) ->
+ Checkpoints = mnesia:dirty_read(?USER_PROGRAM_CHECKPOINTS_TABLE, ProgramId),
+ Sorted = lists:sort(fun( #user_program_checkpoint{event_time=EventTime1}
+ , #user_program_checkpoint{event_time=EventTime2}
+ ) ->
+ EventTime1 > EventTime2
+ end,
+ Checkpoints),
+ case Sorted of
+ [Checkpoint | _] ->
+ {ok, Checkpoint};
+ [] ->
+ {error, not_found}
+ end.
+
-spec delete_program(binary(), binary()) -> { 'ok', binary() } | { 'error', any() }.
delete_program(Username, ProgramName)->
case retrieve_program(Username, ProgramName) of
- {ok, ProgramEntry=#user_program_entry{id=ProgramId}} ->
+ {ok, ProgramEntry=#user_program_entry{ id=ProgramId
+ , program_channel=Channel
+ }} ->
+ ok = automate_channel_engine:delete_channel(Channel),
Transaction = fun() ->
ok = mnesia:delete_object(?USER_PROGRAMS_TABLE,
ProgramEntry, write)
@@ -450,13 +785,36 @@ delete_program(Username, ProgramName)->
{ atomic, Result } ->
Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, mnesia:error_description(Reason)}
end;
X ->
X
end.
+-spec delete_program(binary()) -> ok | { 'error', any() }.
+delete_program(ProgramId)->
+ Transaction = fun() ->
+ case get_program_from_id(ProgramId) of
+ {ok, ProgramEntry=#user_program_entry{ id=ProgramId
+ , program_channel=Channel
+ }} ->
+ ok = automate_channel_engine:delete_channel(Channel),
+ ok = mnesia:delete_object(?USER_PROGRAMS_TABLE,
+ ProgramEntry, write);
+
+ X ->
+ X
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ { atomic, Result } ->
+ Result;
+ { aborted, Reason } ->
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
+ {error, mnesia:error_description(Reason)}
+ end.
+
-spec delete_running_process(binary()) -> ok | {error, not_found}.
delete_running_process(ProcessId) ->
Transaction = fun() ->
@@ -466,7 +824,7 @@ delete_running_process(ProcessId) ->
{ atomic, Result } ->
Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, mnesia:error_description(Reason)}
end.
@@ -481,8 +839,19 @@ get_program_pid(ProgramId) ->
{error, Reason}
end.
+-spec get_program_variables(binary()) -> {'ok', any()}.
+get_program_variables(ProgramId) ->
+ mnesia:ets(fun() ->
+ Vars = mnesia:index_read(?PROGRAM_VARIABLE_TABLE, ProgramId, program_id),
+ Map = maps:from_list(lists:map(
+ fun(#program_variable_table_entry{ id={_, VarName}, value=Value}) ->
+ {VarName, Value}
+ end, Vars)),
+ {ok, Map}
+ end).
--spec get_user_from_pid(pid()) -> { ok, binary() } | {error, not_found}.
+
+-spec get_user_from_pid(pid()) -> { ok, owner_id() } | {error, not_found}.
get_user_from_pid(Pid) ->
%% Look for it as a program (not running thread)
ProgMatchHead = #running_program_entry{ program_id = '$1'
@@ -503,6 +872,7 @@ get_user_from_pid(Pid) ->
, instruction_memory = '_'
, position = '_'
, stats = '_'
+ , direction = '_'
},
ThreadGuard = {'==', '$2', Pid},
ThreadResultColumn = '$1',
@@ -510,30 +880,25 @@ get_user_from_pid(Pid) ->
Transaction = fun() ->
case mnesia:select(?RUNNING_PROGRAMS_TABLE, ProgMatcher) of
[ProgramId] ->
- [#user_program_entry{ user_id=UserId }] = mnesia:read(?USER_PROGRAMS_TABLE, ProgramId),
- { ok, UserId};
+ [#user_program_entry{ owner=Owner }] = mnesia:read(?USER_PROGRAMS_TABLE, ProgramId),
+ { ok, Owner };
[] ->
case mnesia:select(?RUNNING_THREADS_TABLE, ThreadMatcher) of
[ ParentProgramId ] ->
- [#user_program_entry{ user_id=UserId }] = mnesia:read(?USER_PROGRAMS_TABLE, ParentProgramId),
- { ok, UserId};
+ [#user_program_entry{ owner=Owner }] = mnesia:read(?USER_PROGRAMS_TABLE, ParentProgramId),
+ { ok, Owner };
[] ->
{error, not_found}
end
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- {error, Reason}
- end.
+ mnesia:ets(Transaction).
--spec get_program_owner(binary()) -> {'ok', binary() | undefined} | {error, not_found}.
+-spec get_program_owner(binary()) -> {'ok', owner_id() | undefined} | {error, not_found}.
get_program_owner(ProgramId) ->
case get_program_from_id(ProgramId) of
- {ok, #user_program_entry{user_id=UserId}} ->
- {ok, UserId};
+ {ok, #user_program_entry{owner=Owner}} ->
+ {ok, Owner};
{error, Reason} ->
{error, Reason}
end.
@@ -558,10 +923,11 @@ register_program_runner(ProgramId, Pid) ->
{ atomic, Result } ->
Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, mnesia:error_description(Reason)}
end.
+-spec get_program_from_id(binary()) -> {ok, #user_program_entry{}} | {error, not_found}.
get_program_from_id(ProgramId) ->
Transaction = fun() ->
case mnesia:read(?USER_PROGRAMS_TABLE, ProgramId) of
@@ -571,13 +937,7 @@ get_program_from_id(ProgramId) ->
{ok, Program}
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
- {error, mnesia:error_description(Reason)}
- end.
+ mnesia:ets(Transaction).
-spec register_program_tags(binary(), [binary()]) -> 'ok' | {error, not_running}.
register_program_tags(ProgramId, Tags) ->
@@ -599,7 +959,7 @@ register_program_tags(ProgramId, Tags) ->
{ atomic, Result } ->
Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, mnesia:error_description(Reason)}
end.
@@ -612,19 +972,45 @@ get_tags_program_from_id(ProgramId) ->
{ok, Tags}
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
- {error, mnesia:error_description(Reason)}
- end.
+ mnesia:ets(Transaction).
+
+-spec get_logs_from_program_id(binary()) -> {ok, [#user_program_log_entry{}]} | {error, atom()}.
+get_logs_from_program_id(ProgramId) ->
+ Transaction = fun() ->
+ {ok, mnesia:read(?USER_PROGRAM_LOGS_TABLE, ProgramId)}
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
dirty_list_running_programs() ->
{ok, mnesia:dirty_all_keys(?RUNNING_PROGRAMS_TABLE)}.
+
+-spec store_program_event(binary(), any()) -> ok | {error, any()}.
+store_program_event(ProgramId, Event) ->
+ Time = erlang:monotonic_time(),
+ UMI = erlang:unique_integer([monotonic]),
+ EventTag = {Time, UMI},
+
+ T = fun() ->
+ mnesia:write(?USER_PROGRAM_EVENTS_TABLE, #user_program_editor_event{ program_id=ProgramId, event=Event, event_tag=EventTag }, write)
+ end,
+ case mnesia:transaction(T) of
+ {atomic, ok} ->
+ ok;
+ {aborted, Reason} ->
+ {error, Reason}
+ end.
+
+-spec get_program_events(binary()) -> {ok, [#user_program_editor_event{}]} | {error, any()}.
+get_program_events(ProgramId) ->
+ T = fun() ->
+ mnesia:read(?USER_PROGRAM_EVENTS_TABLE, ProgramId)
+ end,
+ {ok, mnesia:ets(T)}.
+
-spec create_thread(binary(), #program_thread{}) -> {ok, thread_id()}.
create_thread(ParentProgramId, #program_thread{ program=Instructions
+ , direction=Direction
, global_memory=Memory
, instruction_memory=InstructionMemory
, position=Position
@@ -637,6 +1023,7 @@ create_thread(ParentProgramId, #program_thread{ program=Instructions
, memory=Memory
, instruction_memory=InstructionMemory
, position=Position
+ , direction=Direction
, stats=#{}
},
@@ -689,6 +1076,7 @@ get_threads_from_program(ParentProgramId) ->
, instruction_memory = '_'
, position = '_'
, stats = '_'
+ , direction = '_'
},
Guard = {'==', '$2', ParentProgramId},
ResultColumn = '$1',
@@ -696,12 +1084,7 @@ get_threads_from_program(ParentProgramId) ->
Transaction = fun() ->
mnesia:select(?RUNNING_THREADS_TABLE, Matcher)
end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- {ok, Result};
- { aborted, Reason } ->
- {error, Reason}
- end.
+ {ok, mnesia:ets(Transaction)}.
dirty_list_running_threads() ->
@@ -738,68 +1121,253 @@ get_thread_from_id(ThreadId) ->
{ok, Thread}
end
end,
+ mnesia:ets(Transaction).
+
+-spec dirty_is_thread_alive(binary()) -> {ok, boolean()}.
+dirty_is_thread_alive(ThreadId) ->
+ case mnesia:dirty_read(?RUNNING_THREADS_TABLE, ThreadId) of
+ [] ->
+ {ok, false};
+ [_Thread] ->
+ {ok, true}
+ end.
+
+-spec get_program_variable(binary(), binary() | {internal, _}) -> {ok, any()} | {error, not_found}.
+get_program_variable(ProgramId, Key) ->
+ Transaction = fun() ->
+ mnesia:read(?PROGRAM_VARIABLE_TABLE, {ProgramId, Key})
+ end,
+ case mnesia:ets(Transaction) of
+ [#program_variable_table_entry{value=Value}] ->
+ {ok, Value};
+ [] ->
+ {error, not_found}
+ end.
+
+-spec log_program_error(#user_program_log_entry{}) -> ok | {error, atom()}.
+log_program_error(LogEntry=#user_program_log_entry{ program_id=ProgramId }) ->
+ {LowWatermark, HighWatermark} = automate_configuration:get_program_logs_watermarks(),
+ Transaction = fun() ->
+ ok = mnesia:write(?USER_PROGRAM_LOGS_TABLE, LogEntry, write),
+
+ ProgramEntries = mnesia:read(?USER_PROGRAM_LOGS_TABLE, ProgramId),
+ case length(ProgramEntries) > HighWatermark of
+ false -> ok;
+ true ->
+ %% Start prunning logs
+ Sorted = lists:sort(fun( #user_program_log_entry{ event_time=Time1 }
+ , #user_program_log_entry{ event_time=Time2 }
+ ) ->
+ Time1 >= Time2
+ end, ProgramEntries),
+ {Kept, _} = lists:split(LowWatermark, Sorted),
+
+ %% Delete old values
+ ok = mnesia:delete(?USER_PROGRAM_LOGS_TABLE, ProgramId, write),
+
+ %% Write new values
+ lists:foreach(fun(Element) ->
+ ok = mnesia:write(?USER_PROGRAM_LOGS_TABLE, Element, write)
+ end, Kept)
+ end
+ end,
case mnesia:transaction(Transaction) of
{ atomic, Result } ->
Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
- {error, mnesia:error_description(Reason)}
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
+ {error, Reason}
end.
+-spec add_user_generated_log(#user_generated_log_entry{}) -> ok | {error, atom()}.
+add_user_generated_log(LogEntry=#user_generated_log_entry{program_id=ProgramId}) ->
+ {LowWatermark, HighWatermark} = automate_configuration:get_program_logs_watermarks(),
+ Transaction = fun() ->
+ ok = mnesia:write(?USER_GENERATED_LOGS_TABLE, LogEntry, write),
+
+ ProgramEntries = mnesia:read(?USER_GENERATED_LOGS_TABLE, ProgramId),
+ case length(ProgramEntries) > HighWatermark of
+ false -> ok;
+ true ->
+ %% Start prunning logs
+ Sorted = lists:sort(fun( #user_generated_log_entry{ event_time=Time1 }
+ , #user_generated_log_entry{ event_time=Time2 }
+ ) ->
+ Time1 >= Time2
+ end, ProgramEntries),
+ {Kept, _} = lists:split(LowWatermark, Sorted),
+
+ %% Delete old values
+ ok = mnesia:delete(?USER_GENERATED_LOGS_TABLE, ProgramId, write),
+
+ %% Write new values
+ lists:foreach(fun(Element) ->
+ ok = mnesia:write(?USER_GENERATED_LOGS_TABLE, Element, write)
+ end, Kept)
+ end
--spec get_program_variable(binary(), atom()) -> {ok, any()} | {error, not_found}.
-get_program_variable(ProgramId, Key) ->
+ end,
+ case mnesia:transaction(Transaction) of
+ { atomic, Result } ->
+ Result;
+ { aborted, Reason } ->
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
+ {error, Reason}
+ end.
+
+-spec get_user_generated_logs(binary()) -> {ok, [#user_generated_log_entry{}]} | {error, _}.
+get_user_generated_logs(ProgramId) ->
Transaction = fun() ->
- mnesia:read(?PROGRAM_VARIABLE_TABLE, {ProgramId, Key})
+ {ok, mnesia:read(?USER_GENERATED_LOGS_TABLE, ProgramId)}
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+
+-spec mark_successful_call_to_bridge(binary(), binary()) -> ok.
+mark_successful_call_to_bridge(ProgramId, _BridgeId) ->
+ CurrentTime = erlang:system_time(second),
+ Transaction = fun() ->
+ case mnesia:read(?USER_PROGRAMS_TABLE, ProgramId) of
+ [Program=#user_program_entry{}] ->
+ ok = mnesia:write( ?USER_PROGRAMS_TABLE
+ , Program#user_program_entry{ last_successful_call_time=CurrentTime }
+ , write
+ );
+ [] ->
+ {error, not_found}
+ end
end,
case mnesia:transaction(Transaction) of
- { atomic, [#program_variable_table_entry{value=Value}] } ->
- {ok, Value};
- { atomic, [] } ->
- {error, not_found};
+ { atomic, Result } ->
+ Result;
+ { aborted, Reason } ->
+ {error, mnesia:error_description(Reason)}
+ end.
+
+-spec mark_failed_call_to_bridge(binary(), binary()) -> ok.
+mark_failed_call_to_bridge(ProgramId, _BridgeId) ->
+ CurrentTime = erlang:system_time(second),
+ Transaction = fun() ->
+ case mnesia:read(?USER_PROGRAMS_TABLE, ProgramId) of
+ [Program=#user_program_entry{}] ->
+ ok = mnesia:write( ?USER_PROGRAMS_TABLE
+ , Program#user_program_entry{ last_failed_call_time=CurrentTime }
+ , write
+ );
+ [] ->
+ {error, not_found}
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ { atomic, Result } ->
+ Result;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
{error, mnesia:error_description(Reason)}
end.
--spec get_userid_from_username(binary()) -> {ok, binary()} | {error, no_user_found}.
+
+-spec get_userid_from_username(binary()) -> {ok, owner_id()} | {error, no_user_found}.
get_userid_from_username(undefined) ->
{ok, undefined};
get_userid_from_username(Username) ->
MatchHead = #registered_user_entry{ id='$1'
- , username='$2'
+ , username='_'
+ , canonical_username='$2'
, password='_'
, email='_'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
%% Check that neither the id, username or email matches another
- Guard = {'==', '$2', Username},
+ Guard = {'==', '$2', automate_storage_utils:canonicalize(Username)},
ResultColumn = '$1',
Matcher = [{MatchHead, [Guard], [ResultColumn]}],
Transaction = fun() ->
mnesia:select(?REGISTERED_USERS_TABLE, Matcher)
end,
+ case mnesia:ets(Transaction) of
+ [Result] ->
+ {ok, {user, Result}};
+ [] ->
+ {error, no_user_found}
+ end.
+
+-spec update_user_settings(binary(), map(), [atom()]) -> ok | {error, _}.
+update_user_settings(UserId, Settings, Permissions) ->
+ Transaction = fun() ->
+ case mnesia:read(?REGISTERED_USERS_TABLE, UserId) of
+ [User] ->
+ case apply_user_settings(User, Settings, Permissions) of
+ {ok, NewUser} ->
+ ok = mnesia:write(?REGISTERED_USERS_TABLE, NewUser, write);
+ {error, Reason} ->
+ {error, Reason}
+ end;
+ [] ->
+ {error, not_found}
+ end
+ end,
case mnesia:transaction(Transaction) of
- { atomic, [Result] } ->
- {ok, Result};
- { atomic, [] } ->
- {error, no_user_found};
+ { atomic, Result } ->
+ Result;
{ aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
+ {error, Reason}
end.
+-spec set_owner_public_listings(owner_id(), Groups :: [binary()]) -> ok | {error, _}.
+set_owner_public_listings(Owner, Groups) ->
+ Entry = #user_profile_listings_entry{ id=Owner
+ , groups=Groups
+ },
+ Transaction = fun() ->
+ ok = mnesia:write(?USER_PROFILE_LISTINGS_TABLE, Entry, write)
+ end,
+ wrap_transaction(mnesia:transaction(Transaction)).
+
+
+-spec get_owner_public_listings(owner_id()) -> {ok, #user_profile_listings_entry{}} | {error, _}.
+get_owner_public_listings(Owner) ->
+ Transaction = fun() ->
+ case mnesia:read(?USER_PROFILE_LISTINGS_TABLE, Owner) of
+ [Result] ->
+ {ok, Result};
+ [] ->
+ {ok, #user_profile_listings_entry{ id=Owner, groups=[] } }
+ end
+ end,
+ wrap_transaction(mnesia:ets(Transaction)).
+
+-spec list_public_collaborators(binary()) -> {ok, [#registered_user_entry{}]} | {error, _}.
+list_public_collaborators(GroupId) ->
+ Transaction = fun() ->
+ Results = lists:filtermap(
+ fun(#user_group_permissions_entry{user_id={user, UserId}}) ->
+ {ok, #user_profile_listings_entry{ groups=ListedGroups }} = get_owner_public_listings({user, UserId}),
+ case lists:any(fun(InList) -> InList == GroupId end, ListedGroups) of
+ false -> false;
+ true ->
+ [User] = mnesia:read(?REGISTERED_USERS_TABLE, UserId),
+ {true, User}
+ end
+ end, mnesia:read(?USER_GROUP_PERMISSIONS_TABLE, GroupId)),
+ {ok, Results}
+ end,
+ wrap_transaction(mnesia:ets(Transaction)).
+
%% Custom signals
--spec create_custom_signal(binary(), binary()) -> {ok, binary()}.
-create_custom_signal(UserId, SignalName) ->
+-spec create_custom_signal(owner_id(), binary()) -> {ok, binary()}.
+create_custom_signal(Owner, SignalName) ->
{ok, Id} = automate_channel_engine:create_channel(),
Entry = #custom_signal_entry{ id=Id
, name=SignalName
- , owner=UserId
+ , owner=Owner
},
Transaction = fun() ->
@@ -814,20 +1382,50 @@ create_custom_signal(UserId, SignalName) ->
end.
--spec list_custom_signals_from_user_id(binary()) -> {ok, [#custom_signal_entry{}]}.
-list_custom_signals_from_user_id(UserId) ->
+-spec list_custom_signals(owner_id()) -> {ok, [#custom_signal_entry{}]}.
+list_custom_signals({OwnerType, OwnerId}) ->
Transaction = fun() ->
%% Find userid with that name
MatchHead = #custom_signal_entry{ id='_'
, name='_'
- , owner='$1'
+ , owner={'$1', '$2'}
},
- Guard = {'==', '$1', UserId},
+ Guards = [ {'==', '$1', OwnerType}
+ , {'==', '$2', OwnerId}
+ ],
ResultColumn = '$_',
- Matcher = [{MatchHead, [Guard], [ResultColumn]}],
+ Matcher = [{MatchHead, Guards, [ResultColumn]}],
{ok, mnesia:select(?CUSTOM_SIGNALS_TABLE, Matcher)}
end,
+ mnesia:ets(Transaction).
+
+%% Group management
+-spec create_group(binary(), binary(), boolean()) -> {ok, #user_group_entry{}} | {error, any()}.
+create_group(Name, AdminUserId, Public) ->
+ Canonicalized = automate_storage_utils:canonicalize(Name),
+ Id = generate_id(),
+ CurrentTime = erlang:system_time(second),
+ Transaction = fun() ->
+ case mnesia:index_read(?USER_GROUPS_TABLE, Name, #user_group_entry.canonical_name) of
+ [] ->
+ Entry = #user_group_entry{ id=Id
+ , name=Name
+ , canonical_name=Canonicalized
+ , public=Public
+ , creation_time=CurrentTime
+ , min_level_for_private_bridge_usage=not_allowed
+ },
+ ok = mnesia:write(?USER_GROUPS_TABLE, Entry, write),
+ ok = mnesia:write(?USER_GROUP_PERMISSIONS_TABLE, #user_group_permissions_entry{ group_id=Id
+ , user_id={user, AdminUserId}
+ , role=admin
+ }, write),
+ {ok, Entry};
+ _ ->
+ {error, already_exists}
+ end
+ end,
case mnesia:transaction(Transaction) of
{ atomic, Result } ->
Result;
@@ -835,6 +1433,182 @@ list_custom_signals_from_user_id(UserId) ->
{error, mnesia:error_description(Reason)}
end.
+
+-spec delete_group(binary()) -> ok | {error, any()}.
+delete_group(GroupId) ->
+ T = fun() ->
+ ok = mnesia:delete(?USER_GROUPS_TABLE, GroupId, write),
+ ok = mnesia:delete(?USER_GROUP_PERMISSIONS_TABLE, GroupId, write)
+ end,
+ wrap_transaction(mnesia:transaction(T)).
+
+-spec update_group_metadata(binary(), group_metadata_edition()) -> ok | {error, any()}.
+update_group_metadata(GroupId, MetadataChanges) ->
+ T = fun() ->
+ [Group] = mnesia:read(?USER_GROUPS_TABLE, GroupId),
+ NewGroup = apply_group_metadata_changes(Group, MetadataChanges),
+ mnesia:write(?USER_GROUPS_TABLE, NewGroup, write)
+ end,
+ wrap_transaction(mnesia:transaction(T)).
+
+-spec get_user_groups(owner_id()) -> {ok, [{#user_group_entry{}, user_in_group_role()}, ...]} | {error, any()}.
+get_user_groups(UserId) ->
+ Transaction = fun() ->
+ Permissions = mnesia:index_read(?USER_GROUP_PERMISSIONS_TABLE, UserId, #user_group_permissions_entry.user_id),
+ Groups = lists:map(fun(#user_group_permissions_entry{ group_id=GroupId, role=Role }) ->
+ [Group] = mnesia:read(?USER_GROUPS_TABLE, GroupId),
+ {Group, Role}
+ end, Permissions),
+ {ok, Groups}
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec get_group_by_name(binary()) -> {ok, #user_group_entry{}} | {error, any()}.
+get_group_by_name(GroupName) ->
+ CanonicalizedName = automate_storage_utils:canonicalize(GroupName),
+ Transaction = fun() ->
+ case mnesia:index_read(?USER_GROUPS_TABLE
+ , CanonicalizedName
+ , #user_group_entry.canonical_name) of
+ [Group] ->
+ {ok, Group};
+ [] ->
+ {error, not_found}
+ end
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec is_allowed_to_read_in_group(owner_id(), binary()) -> true | false.
+is_allowed_to_read_in_group({group, GroupId}, GroupId) ->
+ true;
+is_allowed_to_read_in_group(AccessorId, GroupId) ->
+ Transaction = fun() ->
+ lists:any(fun(#user_group_permissions_entry{user_id=UserId}) ->
+ UserId =:= AccessorId
+ end, mnesia:read(?USER_GROUP_PERMISSIONS_TABLE, GroupId))
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec is_allowed_to_write_in_group(owner_id(), binary()) -> true | false.
+is_allowed_to_write_in_group({group, GroupId}, GroupId) ->
+ true;
+is_allowed_to_write_in_group(AccessorId, GroupId) ->
+ Transaction = fun() ->
+ lists:any(fun(#user_group_permissions_entry{user_id=UserId, role=Role}) ->
+ (UserId == AccessorId)
+ and
+ ( (Role == admin) or (Role == editor) )
+ end, mnesia:read(?USER_GROUP_PERMISSIONS_TABLE, GroupId))
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec is_allowed_to_admin_in_group(owner_id(), binary()) -> true | false.
+is_allowed_to_admin_in_group({group, GroupId}, GroupId) ->
+ true;
+is_allowed_to_admin_in_group(AccessorId, GroupId) ->
+ Transaction = fun() ->
+ lists:any(fun(#user_group_permissions_entry{user_id=UserId, role=Role}) ->
+ (UserId == AccessorId)
+ and
+ ( Role == admin )
+ end, mnesia:read(?USER_GROUP_PERMISSIONS_TABLE, GroupId))
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec can_user_admin_as(owner_id(), owner_id()) -> true | false.
+can_user_admin_as(AccessorId, {group, GroupId}) ->
+ is_allowed_to_admin_in_group(AccessorId, GroupId);
+can_user_admin_as({user, UserId}, {user, UserId}) ->
+ true;
+can_user_admin_as({user, _UserId}, {user, _AnotherUser}) ->
+ false.
+
+-spec can_user_edit_as(owner_id(), owner_id()) -> true | false.
+can_user_edit_as(AccessorId, {group, GroupId}) ->
+ is_allowed_to_write_in_group(AccessorId, GroupId);
+can_user_edit_as({user, UserId}, {user, UserId}) ->
+ true;
+can_user_edit_as({user, _UserId}, {user, _AnotherUser}) ->
+ false.
+
+-spec can_user_view_as(owner_id(), owner_id()) -> true | false.
+can_user_view_as(AccessorId, {group, GroupId}) ->
+ is_allowed_to_read_in_group(AccessorId, GroupId);
+can_user_view_as({user, UserId}, {user, UserId}) ->
+ true;
+can_user_view_as({user, _UserId}, {user, _AnotherUser}) ->
+ false.
+
+-spec list_collaborators({group, binary()}) -> {ok, [{#user_program_entry{}, user_in_group_role()}, ...]} | {error, any()}.
+list_collaborators({group, GroupId}) ->
+ Transaction = fun() ->
+ Results = lists:map(fun(#user_group_permissions_entry{user_id={user, UserId}, role=Role}) ->
+ [User] = mnesia:read(?REGISTERED_USERS_TABLE, UserId),
+ {User, Role}
+ end, mnesia:read(?USER_GROUP_PERMISSIONS_TABLE, GroupId)),
+ {ok, Results}
+ end,
+ wrap_transaction(mnesia:activity(ets, Transaction)).
+
+-spec add_collaborators({group, binary()}, [{ Id :: binary(), Role :: user_in_group_role() }]) -> ok | {error, any()}.
+add_collaborators({group, GroupId}, Collaborators) ->
+ Transaction = fun() ->
+ ok = lists:foreach(fun({ CollaboratorId, CollaboratorRole }) ->
+ ok = mnesia:write(?USER_GROUP_PERMISSIONS_TABLE
+ , #user_group_permissions_entry{ group_id=GroupId
+ , user_id={user, CollaboratorId}
+ , role=CollaboratorRole
+ }
+ , write)
+ end, Collaborators)
+ end,
+ wrap_transaction(mnesia:transaction(Transaction)).
+
+-spec update_collaborators({group, binary()}, [{ Id :: binary(), Role :: user_in_group_role() }]) -> ok | {error, any()}.
+update_collaborators({group, GroupId}, Collaborators) ->
+ Transaction = fun() ->
+ %% Delete all collaborators
+ ok = mnesia:delete(?USER_GROUP_PERMISSIONS_TABLE, GroupId, write),
+ %% And add new ones
+ add_collaborators({group, GroupId}, Collaborators)
+ end,
+ wrap_transaction(mnesia:transaction(Transaction)).
+
+-spec is_user_allowed_to_create_public_bridges(OwnerId :: owner_id()) -> {ok, boolean()} | {error, not_found}.
+is_user_allowed_to_create_public_bridges({user, UserId}) ->
+ %% Only admins can create public bridges
+
+ case get_user(UserId) of
+ {ok, #registered_user_entry{ is_admin=IsAdmin }} ->
+ {ok, IsAdmin};
+ {error, Reason} ->
+ {error, Reason}
+ end;
+is_user_allowed_to_create_public_bridges({group, _}) ->
+ %% No group is allowed to create public bridges
+ {ok, false}.
+
+-spec is_user_allowed_to_connect_to_bridges_in_group(OwnerId :: owner_id(), GroupId :: binary()) -> { ok, boolean() }.
+is_user_allowed_to_connect_to_bridges_in_group({group, GroupId}, GroupId) ->
+ true;
+is_user_allowed_to_connect_to_bridges_in_group(OwnerId, GroupId) ->
+ Transaction = fun() ->
+ Permissions = mnesia:read(?USER_GROUP_PERMISSIONS_TABLE, GroupId),
+ [#user_group_entry{min_level_for_private_bridge_usage=MinLevel}] = mnesia:read(?USER_GROUPS_TABLE, GroupId),
+ Roles = lists:filtermap(fun(#user_group_permissions_entry{ role=Role
+ , user_id=UserId }) ->
+ case UserId of
+ OwnerId -> {true, Role};
+ _ -> false
+ end
+ end, Permissions),
+
+ {ok, lists:any(fun(Role) ->
+ automate_storage_utils:role_has_min_level_in_group(Role, MinLevel)
+ end, Roles)}
+ end,
+ mnesia:activity(ets, Transaction).
+
%% Exposed startup entrypoint
start_link() ->
start_coordinator().
@@ -852,14 +1626,53 @@ register_table(_TableName, _RecordDef) ->
%%====================================================================
%% Internal functions
%%====================================================================
+wrap_transaction(TransactionResult) ->
+ case TransactionResult of
+ {aborted, Reason} ->
+ {error, Reason};
+ {atomic, Result} ->
+ Result;
+ Result ->
+ Result
+ end.
+
+gen_salt() ->
+ gen_salt(?PASSWORD_HASHING_SALTLEN).
+gen_salt(SaltLen) ->
+ crypto:strong_rand_bytes(SaltLen).
+
+-spec cipher_password(binary()) -> binary() | string().
cipher_password(Plaintext) ->
Password = Plaintext,
- Opslimit = libsodium_crypto_pwhash:opslimit_interactive(), % Minimal recommended
- Memlimit = libsodium_crypto_pwhash:memlimit_interactive(), % 64MiB
- HashedPassword = libsodium_crypto_pwhash:str(Password, Opslimit, Memlimit),
- HashedPassword.
+ Salt = gen_salt(),
+
+ {ok, HashResult} = eargon2:hash(?PASSWORD_HASHING_OPS_LIMIT, ?PASSWORD_HASHING_MEM_LIMIT, ?PASSWORD_HASHING_PARALLELISM,
+ Password, Salt,
+ ?PASSWORD_HASHING_HASHLEN,
+ ?EARGON2_RESULT_TYPE_ENCODED, ?EARGON2_HASH_TYPE_ARGON2_I, ?EARGON2_VERSION_NUMBER),
+
+ HashResult.
-add_token_to_user(UserId, SessionToken) ->
+-spec verify_passwd_hash(binary() | string(), binary()) -> ok | {error, number()}.
+verify_passwd_hash(Hash, Password) when is_binary(Hash) ->
+ %% Fix mismatch between libsodium and eargon2
+ verify_passwd_hash(binary:bin_to_list(Hash), Password);
+
+verify_passwd_hash(Hash=("$argon2i$" ++ _), Password) ->
+ %% Handle Argon2 - I
+ eargon2:verify_2i(Hash, Password);
+
+verify_passwd_hash(Hash=("$argon2d$" ++ _), Password) ->
+ %% Handle Argon2 - D
+ eargon2:verify_2d(Hash, Password);
+
+verify_passwd_hash(Hash=("$argon2id$" ++ _), Password) ->
+ %% Handle Argon2 - ID
+ eargon2:verify_2id(Hash, Password).
+
+
+-spec add_token_to_user(binary(), binary(), session_scope(), session_expiration_time()) -> ok.
+add_token_to_user(UserId, SessionToken, Scope, Expiration) ->
StartTime = erlang:system_time(second),
Transaction = fun() ->
mnesia:write(?USER_SESSIONS_TABLE
@@ -867,21 +1680,89 @@ add_token_to_user(UserId, SessionToken) ->
, user_id=UserId
, session_start_time=StartTime
, session_last_used_time=0
+ , session_scope=Scope
+ , session_expiration_time=Expiration
}
, write)
end,
{atomic, Result} = mnesia:transaction(Transaction),
Result.
-get_userid_and_password_from_username(Username) ->
+get_userid_sessions(UserId) ->
+ %% User session queries
+ SessionMatchHead = #user_session_entry{ session_id='_'
+ , user_id='$1'
+ , session_start_time='_'
+ , session_last_used_time='_'
+ , session_scope='_'
+ , session_expiration_time='_'
+ },
+ SessionResultColumn = '$_',
+ SessionMatcher = [{ SessionMatchHead
+ , [{ '==', '$1', UserId }]
+ , [SessionResultColumn]
+ }],
+ mnesia:select(?USER_SESSIONS_TABLE, SessionMatcher).
+
+search_users_iter('$end_of_table', Acc, _QueryRe) ->
+ Acc;
+search_users_iter(Key, Acc, QueryRe) ->
+ [Element] = mnesia:read(?REGISTERED_USERS_TABLE, Key),
+ Accumulated = case user_match_query(Element, QueryRe) of
+ true ->
+ [Element | Acc];
+ false ->
+ Acc
+ end,
+ search_users_iter(mnesia:next(?REGISTERED_USERS_TABLE, Key), Accumulated, QueryRe).
+
+
+user_match_query(#registered_user_entry{status=mail_not_verified}, _QueryRe) ->
+ false;
+user_match_query(#registered_user_entry{username=Username, email=Email}, QueryRe) ->
+ case re:run(Username, QueryRe) of
+ {match, _} ->
+ true;
+ nomatch ->
+ case re:run(Email, QueryRe) of
+ {match, _} ->
+ true;
+ nomatch ->
+ false
+ end
+ end.
+
+
+query_to_re(Query) ->
+ Parts = binary:split(escape_re(Query), [<<" ">>, <<"*">>], [global, trim_all]),
+ join_with([<<"">> | Parts], <<".*">>).
+
+escape_re(Expression) ->
+ %% Note that Whitespaces and Askterisks (*) are NOT escaped
+ re:replace(Expression, "[-[\\]{}()+?.,\\^$|#]", "\\\\&", [{return, binary}, global]).
+
+-spec join_with([binary(), ...], binary()) -> [binary()].
+join_with(Parts, Joiner) when is_list(Parts) and is_binary(Joiner) ->
+ interleave(Parts, Joiner, []).
+
+interleave([], _Joiner, Acc) ->
+ Acc;
+interleave([H|T], Joiner, Acc) ->
+ interleave(T, Joiner, [H | [ Joiner | Acc]]).
+
+get_user_from_username(Username) ->
MatchHead = #registered_user_entry{ id='$1'
- , username='$2'
+ , username='_'
+ , canonical_username='$2'
, password='$3'
, email='_'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
- Guard = {'==', '$2', Username},
+ Guard = {'==', '$2', automate_storage_utils:canonicalize(Username)},
ResultColumn = '$1',
Matcher = [{MatchHead, [Guard], [ResultColumn]}],
@@ -893,30 +1774,98 @@ get_userid_and_password_from_username(Username) ->
[]
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, [Result] } ->
+ case mnesia:ets(Transaction) of
+ [Result] ->
{ok, Result};
- { atomic, [] } ->
- {error, no_user_found};
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
+ [] ->
+ {error, no_user_found}
+ end.
+
+get_user_from_email(Email) ->
+ MatchHead = #registered_user_entry{ id='$1'
+ , username='_'
+ , canonical_username='_'
+ , password='$3'
+ , email='$2'
+ , status='_'
+ , registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
+ },
+ Guard = {'==', '$2', Email},
+ ResultColumn = '$1',
+ Matcher = [{MatchHead, [Guard], [ResultColumn]}],
+
+ Transaction = fun() ->
+ case mnesia:select(?REGISTERED_USERS_TABLE, Matcher) of
+ [UserId] ->
+ mnesia:read(?REGISTERED_USERS_TABLE, UserId);
+ [] ->
+ []
+ end
+ end,
+ case mnesia:ets(Transaction) of
+ [Result] ->
+ {ok, Result};
+ [] ->
+ {error, no_user_found}
end.
+apply_user_settings(User, Settings, Permissions) ->
+ apply_user_settings(User, Settings, Permissions, []).
+
+apply_user_settings(User, _Settings, [], []) ->
+ %% No more permissions to apply, no errors
+ {ok, User};
+apply_user_settings(_User, _Settings, [], ErrorAcc) ->
+ %% No more permissions to apply, errors found
+ {error, {data_error, ErrorAcc}};
+apply_user_settings(User, Settings, [ user_permissions | T ], ErrorAcc) ->
+ {NewUser, NewErrors} = apply_user_permissions(User, Settings),
+ apply_user_settings(NewUser, Settings, T, NewErrors ++ ErrorAcc).
+
+apply_user_permissions(User, Settings) ->
+ Errors = [],
+ {User1, Errors1} = case Settings of
+ #{ <<"is_advanced">> := IsAdvanced } when is_boolean(IsAdvanced) ->
+ { User#registered_user_entry{ is_advanced=IsAdvanced}, Errors };
+ #{ <<"is_advanced">> := _IsAdvanced } ->
+ %% Is advanced found, but it's not boolean
+ { User, [ { bad_type, is_advanced } | Errors ] };
+ #{} ->
+ { User, Errors }
+ end,
+ {User2, Errors2} = case Settings of
+ #{ <<"is_in_preview">> := IsInPreview } when is_boolean(IsInPreview) ->
+ { User1#registered_user_entry{ is_in_preview=IsInPreview}, Errors1 };
+ #{ <<"is_in_preview">> := _IsInPreview } ->
+ %% In preview found, but it's not boolean
+ { User1, [ { bad_type, is_in_preview } | Errors1 ] };
+ #{} ->
+ { User1, Errors1 }
+ end,
+ {User2, Errors2}.
+
-spec create_verification_entry(binary(), verification_type()) -> {ok, binary()} | {error, _}.
create_verification_entry(UserId, VerificationType) ->
VerificationId = generate_id(),
+ CurrentTime = erlang:system_time(second),
+
Transaction = fun() ->
ok = mnesia:write(?USER_VERIFICATION_TABLE,
#user_verification_entry{ verification_id=VerificationId
, user_id=UserId
, verification_type=VerificationType
+ , creation_time=CurrentTime
+ , used=false
}, write)
end,
case mnesia:transaction(Transaction) of
{ atomic, ok } ->
{ok, VerificationId};
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, Reason}
end.
@@ -931,16 +1880,13 @@ check_verification_code(VerificationCode, VerificationType) ->
{error, {invalid_verification_type, OtherVerificationType}}
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, {error, {invalid_verification_type, OtherVerificationType}} } ->
+ case mnesia:ets(Transaction) of
+ {error, {invalid_verification_type, OtherVerificationType}} ->
io:fwrite("[Storage] Expected type ~p on verification, found: ~p~n",
[VerificationType, OtherVerificationType]),
{error, invalid_verification_type};
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
- {error, Reason}
+ Result ->
+ Result
end.
store_new_monitor(Monitor) ->
@@ -953,16 +1899,21 @@ store_new_monitor(Monitor) ->
Result.
retrieve_monitors_list_from_username(Username) ->
+ io:fwrite("\033[7m[retrieve_monitors_list_from_username] To be deprecated\033[0m~n"),
Transaction = fun() ->
%% Find userid with that name
UserMatchHead = #registered_user_entry{ id='$1'
- , username='$2'
+ , username='_'
+ , canonical_username='$2'
, password='_'
, email='_'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
- UserGuard = {'==', '$2', Username},
+ UserGuard = {'==', '$2', automate_storage_utils:canonicalize(Username)},
UserResultColumn = '$1',
UserMatcher = [{UserMatchHead, [UserGuard], [UserResultColumn]}],
@@ -973,7 +1924,7 @@ retrieve_monitors_list_from_username(Username) ->
%% Find program with userId and name
MonitorMatchHead = #monitor_entry{ id='$1'
- , user_id='$2'
+ , owner={user, '$2'}
, name='_'
, type='_'
, value='_'
@@ -986,13 +1937,11 @@ retrieve_monitors_list_from_username(Username) ->
[mnesia:read(?USER_MONITORS_TABLE, ResultId) || ResultId <- Results]
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, { error, Reason }} ->
+ case mnesia:ets(Transaction) of
+ { error, Reason } ->
{error, Reason };
- { atomic, Result } ->
- {ok, Result};
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
+ Result ->
+ {ok, Result}
end.
store_new_program(UserProgram) ->
@@ -1014,16 +1963,21 @@ store_new_thread(UserThread) ->
Result.
retrieve_program(Username, ProgramName) ->
+ io:fwrite("\033[7m[retrieve_program(Username, ProgramName)] To be deprecated\033[0m~n"),
Transaction = fun() ->
%% Find userid with that name
UserMatchHead = #registered_user_entry{ id='$1'
- , username='$2'
+ , username='_'
+ , canonical_username='$2'
, password='_'
, email='_'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
- UserGuard = {'==', '$2', Username},
+ UserGuard = {'==', '$2', automate_storage_utils:canonicalize(Username)},
UserResultColumn = '$1',
UserMatcher = [{UserMatchHead, [UserGuard], [UserResultColumn]}],
@@ -1034,12 +1988,18 @@ retrieve_program(Username, ProgramName) ->
%% Find program with userId and name
ProgramMatchHead = #user_program_entry{ id='$1'
- , user_id='$2'
+ , owner={user, '$2'}
, program_name='$3'
, program_type='_'
, program_parsed='_'
, program_orig='_'
, enabled='_'
+ , program_channel='_'
+ , creation_time='_'
+ , last_upload_time='_'
+ , last_successful_call_time='_'
+ , last_failed_call_time='_'
+ , visibility='_'
},
ProgramGuard = {'andthen'
, {'==', '$2', UserId}
@@ -1056,26 +2016,29 @@ retrieve_program(Username, ProgramName) ->
end
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, [Result] } ->
+ case mnesia:ets(Transaction) of
+ [Result] ->
{ok, Result};
- { atomic, [] } ->
- {error, not_found};
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
+ [] ->
+ {error, not_found}
end.
retrieve_program_list_from_username(Username) ->
+ io:fwrite("\033[7m[retrieve_program_list_from_username] To be deprecated\033[0m~n"),
Transaction = fun() ->
%% Find userid with that name
UserMatchHead = #registered_user_entry{ id='$1'
- , username='$2'
+ , username='_'
+ , canonical_username='$2'
, password='_'
, email='_'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
- UserGuard = {'==', '$2', Username},
+ UserGuard = {'==', '$2', automate_storage_utils:canonicalize(Username)},
UserResultColumn = '$1',
UserMatcher = [{UserMatchHead, [UserGuard], [UserResultColumn]}],
@@ -1086,12 +2049,18 @@ retrieve_program_list_from_username(Username) ->
%% Find program with userId and name
ProgramMatchHead = #user_program_entry{ id='$1'
- , user_id='$2'
+ , owner={user, '$2'}
, program_name='_'
, program_type='_'
, program_parsed='_'
, program_orig='_'
, enabled='_'
+ , program_channel='_'
+ , creation_time='_'
+ , last_upload_time='_'
+ , last_successful_call_time='_'
+ , last_failed_call_time='_'
+ , visibility='_'
},
ProgramGuard = {'==', '$2', UserId},
ProgramResultsColumn = '$1',
@@ -1101,40 +2070,11 @@ retrieve_program_list_from_username(Username) ->
[mnesia:read(?USER_PROGRAMS_TABLE, ResultId) || ResultId <- Results]
end
end,
- case mnesia:transaction(Transaction) of
- { atomic, { error, Reason }} ->
- {error, Reason };
- { atomic, Result } ->
- {ok, Result};
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
- end.
-
-retrieve_program_list_from_userid(UserId) ->
- Transaction = fun() ->
- %% Find program with userId and name
- ProgramMatchHead = #user_program_entry{ id='$1'
- , user_id='$2'
- , program_name='$3'
- , program_type='_'
- , program_parsed='_'
- , program_orig='_'
- , enabled='_'
- },
- ProgramGuard = {'==', '$2', UserId},
- ProgramResultsColumn = '$1',
- ProgramMatcher = [{ProgramMatchHead, [ProgramGuard], [ProgramResultsColumn]}],
-
- Results = mnesia:select(?USER_PROGRAMS_TABLE, ProgramMatcher),
- [mnesia:read(?USER_PROGRAMS_TABLE, ResultId) || ResultId <- Results]
- end,
- case mnesia:transaction(Transaction) of
- { atomic, { error, Reason }} ->
+ case mnesia:ets(Transaction) of
+ { error, Reason } ->
{error, Reason };
- { atomic, Result } ->
- {ok, Result};
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
+ Result ->
+ {ok, Result}
end.
-spec store_new_program_content(binary(), binary(), #stored_program_content{}) -> { 'ok', binary() } | { 'error', any() }.
@@ -1142,17 +2082,25 @@ store_new_program_content(Username, ProgramName,
#stored_program_content{ orig=ProgramOrig
, parsed=ProgramParsed
, type=ProgramType
+ , pages=Pages
})->
+ io:fwrite("\033[7m[store_new_program_content(Username, ProgramName,...)] To be deprecated\033[0m~n"),
+
+ CurrentTime = erlang:system_time(second),
Transaction = fun() ->
%% Find userid with that name
UserMatchHead = #registered_user_entry{ id='$1'
- , username='$2'
+ , username='_'
+ , canonical_username='$2'
, password='_'
, email='_'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
- UserGuard = {'==', '$2', Username},
+ UserGuard = {'==', '$2', automate_storage_utils:canonicalize(Username)},
UserResultColumn = '$1',
UserMatcher = [{UserMatchHead, [UserGuard], [UserResultColumn]}],
@@ -1163,12 +2111,18 @@ store_new_program_content(Username, ProgramName,
%% Find program with userId and name
ProgramMatchHead = #user_program_entry{ id='$1'
- , user_id='$2'
+ , owner={user, '$2'}
, program_name='$3'
, program_type='_'
, program_parsed='_'
, program_orig='_'
, enabled='_'
+ , program_channel='_'
+ , creation_time='_'
+ , last_upload_time='_'
+ , last_successful_call_time='_'
+ , last_failed_call_time='_'
+ , visibility='_'
},
ProgramGuard = {'andthen'
, {'==', '$2', UserId}
@@ -1180,15 +2134,37 @@ store_new_program_content(Username, ProgramName,
[] ->
[];
- [Program] ->
+ [Program=#user_program_entry{id=ProgramId}] ->
ok = mnesia:write(?USER_PROGRAMS_TABLE,
- Program#user_program_entry{ user_id=UserId
+ Program#user_program_entry{ owner={user, UserId}
, program_name=ProgramName
, program_type=ProgramType
, program_parsed=ProgramParsed
, program_orig=ProgramOrig
+ , last_upload_time=CurrentTime
}, write),
- { ok, Program#user_program_entry.id }
+
+ ok = mnesia:delete(?USER_PROGRAM_EVENTS_TABLE, ProgramId, write),
+
+ %% Refresh pages
+ %% Remove old pages
+ PagesInDb = mnesia:index_read(?PROGRAM_PAGES_TABLE, ProgramId, program_id),
+ ok = lists:foreach(fun (PageInDb) ->
+ ok = mnesia:delete_object(?PROGRAM_PAGES_TABLE, PageInDb, write)
+ end,
+ PagesInDb),
+
+ %% Add new pages
+ ok = lists:foreach(fun({Path, Page}) ->
+ ok = mnesia:write(?PROGRAM_PAGES_TABLE
+ , #program_pages_entry{ page_id={ ProgramId, Path }
+ , program_id=ProgramId
+ , contents=Page
+ }
+ , write)
+ end, maps:to_list(Pages)),
+
+ { ok, ProgramId }
end
end
end,
@@ -1202,34 +2178,93 @@ store_new_program_content(Username, ProgramName,
end.
+-spec store_new_program_content(binary(), #stored_program_content{}) -> { 'ok', binary() } | { 'error', any() }.
+store_new_program_content(ProgramId,
+ #stored_program_content{ orig=ProgramOrig
+ , parsed=ProgramParsed
+ , type=ProgramType
+ , pages=Pages
+ })->
+ CurrentTime = erlang:system_time(second),
+ Transaction = fun() ->
+ case mnesia:read(?USER_PROGRAMS_TABLE, ProgramId) of
+ [] ->
+ [];
+
+ [Program=#user_program_entry{id=ProgramId}] ->
+ ok = mnesia:write(?USER_PROGRAMS_TABLE,
+ Program#user_program_entry{ program_type=ProgramType
+ , program_parsed=ProgramParsed
+ , program_orig=ProgramOrig
+ , last_upload_time=CurrentTime
+ }, write),
+ ok = mnesia:delete(?USER_PROGRAM_EVENTS_TABLE, ProgramId, write),
+
+ %% Refresh pages
+ %% Remove old pages
+ PagesInDb = mnesia:index_read(?PROGRAM_PAGES_TABLE, ProgramId, program_id),
+ ok = lists:foreach(fun (PageInDb) ->
+ ok = mnesia:delete_object(?PROGRAM_PAGES_TABLE, PageInDb, write)
+ end,
+ PagesInDb),
+
+ %% Add new pages
+ ok = lists:foreach(fun({Path, Contents}) ->
+ ok = mnesia:write(?PROGRAM_PAGES_TABLE
+ , #program_pages_entry{ page_id={ ProgramId, Path }
+ , program_id=ProgramId
+ , contents= Contents
+ }
+ , write)
+ end, maps:to_list(Pages)),
+
+ { ok, ProgramId }
+ end
+ end,
+ case mnesia:transaction(Transaction) of
+ { atomic, {ok, Result} } ->
+ {ok, Result};
+ { atomic, [] } ->
+ {error, not_found};
+ { aborted, Reason } ->
+ {error, mnesia:error_description(Reason)}
+ end.
+
+
save_unique_user(UserData) ->
#registered_user_entry{ id=UserId
- , username=Username
+ , canonical_username=CanonicalUsername
, email=Email
} = UserData,
MatchHead = #registered_user_entry{ id='$1'
- , username='$2'
+ , username='_'
+ , canonical_username='$2'
, password='_'
, email='$3'
, status='_'
, registration_time='_'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
%% Check that neither the id, username or email matches another
GuardId = {'==', '$1', UserId},
- GuardUsername = {'==', '$2', Username},
+ GuardUsername = {'==', '$2', CanonicalUsername},
GuardEmail = {'==', '$3', Email},
Guard = {'orelse', GuardId, GuardUsername, GuardEmail},
- ResultColumn = '$1',
+ ResultColumn = '$2',
Matcher = [{MatchHead, [Guard], [ResultColumn]}],
Transaction = fun() ->
case mnesia:select(?REGISTERED_USERS_TABLE, Matcher) of
[] ->
mnesia:write(?REGISTERED_USERS_TABLE, UserData, write);
+ [CanonicalUsername | _] ->
+ {error, {colliding_element, username} };
_ ->
- {error, colliding_element }
+ {error, {colliding_element, email}}
end
end,
case mnesia:transaction(Transaction) of
@@ -1243,17 +2278,13 @@ get_running_program_id(ProgramId) ->
Transaction = fun() ->
mnesia:read(?RUNNING_PROGRAMS_TABLE, ProgramId)
end,
- case mnesia:transaction(Transaction) of
- { atomic, Result } ->
- Result;
- { aborted, Reason } ->
- {error, mnesia:error_description(Reason)}
- end.
+ mnesia:ets(Transaction).
--spec set_program_variable(binary(), atom(), any()) -> ok | {error, any()}.
+-spec set_program_variable(binary(), binary() | { internal, _ }, any()) -> ok | {error, any()}.
set_program_variable(ProgramId, Key, Value) ->
Transaction = fun() ->
mnesia:write(?PROGRAM_VARIABLE_TABLE, #program_variable_table_entry{ id={ProgramId, Key}
+ , program_id=ProgramId
, value=Value
},
write)
@@ -1262,10 +2293,74 @@ set_program_variable(ProgramId, Key, Value) ->
{ atomic, ok } ->
ok;
{ aborted, Reason } ->
- io:format("Error: ~p~n", [mnesia:error_description(Reason)]),
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
{error, mnesia:error_description(Reason)}
end.
+-spec delete_program_variable(binary(), binary()) -> ok | {error, any()}.
+delete_program_variable(ProgramId, Key) ->
+ Transaction = fun() ->
+ mnesia:delete(?PROGRAM_VARIABLE_TABLE, {ProgramId, Key},
+ write)
+ end,
+ case mnesia:transaction(Transaction) of
+ { atomic, ok } ->
+ ok;
+ { aborted, Reason } ->
+ io:format("[~p:~p] Error: ~p~n", [?MODULE, ?LINE, mnesia:error_description(Reason)]),
+ {error, Reason}
+ end.
+
+-spec set_widget_value(ProgramId :: binary(), WidgetId :: binary(), Value :: any()) -> ok.
+set_widget_value(ProgramId, WidgetId, Value) ->
+ Transaction = fun() ->
+ mnesia:write(?PROGRAM_WIDGET_VALUE_TABLE, #program_widget_value_entry{ widget_id={ProgramId, WidgetId}
+ , program_id=ProgramId
+ , value=Value
+ },
+ write)
+ end,
+ wrap_transaction(mnesia:transaction(Transaction)).
+
+-spec get_widget_values_in_program(ProgramId :: binary()) -> {ok, #{ binary() => any() }}.
+get_widget_values_in_program(ProgramId) ->
+ T = fun() ->
+ mnesia:index_read(?PROGRAM_WIDGET_VALUE_TABLE, ProgramId, program_id)
+ end,
+ case wrap_transaction(mnesia:ets(T)) of
+ {error, Reason} ->
+ {error, Reason};
+ Values ->
+ MappedValues = maps:from_list(lists:map(fun(#program_widget_value_entry{ widget_id={ _, WidgetId }, value=Value }) ->
+ { WidgetId, Value }
+ end, Values)),
+ {ok, MappedValues}
+ end.
+
+
+
+-spec apply_group_metadata_changes(#user_group_entry{}, group_metadata_edition()) -> #user_group_entry{}.
+apply_group_metadata_changes(Group, MetadataChanges) ->
+ G1 = apply_group_metadata_public_changes(Group, MetadataChanges),
+ G2 = apply_group_metadata_min_level_changes(G1, MetadataChanges),
+ G2.
+
+apply_group_metadata_public_changes(Group=#user_group_entry{}, #{ public := IsPublic }) ->
+ Group#user_group_entry{ public=IsPublic };
+apply_group_metadata_public_changes(Group, _) ->
+ Group.
+
+apply_group_metadata_min_level_changes(Group=#user_group_entry{}, #{ min_level_for_private_bridge_usage := MinLevel } ) ->
+ Group#user_group_entry{ min_level_for_private_bridge_usage=MinLevel };
+apply_group_metadata_min_level_changes(Group, _ ) ->
+ Group.
+
+-spec parse_visibility(binary()) -> user_program_visibility().
+parse_visibility(<<"public">>) -> public;
+parse_visibility(<<"private">>) -> private;
+parse_visibility(<<"shareable">>) -> shareable;
+parse_visibility(X) when is_atom(X) -> X.
+
%%====================================================================
%% Startup functions
%%====================================================================
@@ -1273,8 +2368,11 @@ start_coordinator() ->
Primary = automate_configuration:get_sync_primary(),
IsPrimary = automate_configuration:is_node_primary(node()),
+ io:fwrite("[~p] Starting coordination~n", [?MODULE]),
+
Spawner = self(),
Coordinator = spawn_link(fun() ->
+ io:fwrite("[~p] Stopping mnesia for synchronization~n", [?MODULE]),
mnesia:stop(),
register(?SERVER, self()),
@@ -1287,11 +2385,21 @@ start_coordinator() ->
true ->
ok = prepare_nodes(SyncPeers),
ok = mnesia:start(),
+
+ %% Subscribe to fatal mnesia events
+ {ok, _} = mnesia:subscribe(system),
+ _ = mnesia:set_debug_level(verbose),
+
NonPrimaryList = sets:to_list(NonPrimaries),
lists:foreach(fun (Node) ->
ok = add_mnesia_node(Node)
end, NonPrimaryList),
- mnesia:info(),
+
+ %% This might fail when some nodes are blocked.
+ %% It is handled as the information itself is not needed.
+ try mnesia:info() of _ -> ok
+ catch _:_:_ -> io:fwrite("Error getting mnesia info~n")
+ end,
io:fwrite("SP: ~p~n", [SyncPeers]),
ok = build_tables(SyncPeers),
@@ -1304,13 +2412,29 @@ start_coordinator() ->
ok
end,
+ io:fwrite("[~p~p] Waiting for tables before setup: ~0tp~n",
+ [ ?MODULE, ?LINE, mnesia:system_info(tables) ]),
+ ok = mnesia:wait_for_tables(mnesia:system_info(tables), automate_configuration:get_table_wait_time()),
Spawner ! {self(), ready},
- coordinate_loop(Primary)
+
+ %% This process cannot longer work if mnesia goes down
+ true = link(whereis(mnesia_sup)),
+ case IsPrimary of
+ true -> coordinate_loop_primary();
+ false ->
+ %% Link to the primary's coordinator process
+ PrimaryProcess = rpc:call(Primary, erlang, whereis, [?SERVER]),
+ io:fwrite("[~p:~p] Linking to primary: ~p~n", [?MODULE, ?LINE, PrimaryProcess]),
+ erlang:monitor(process, PrimaryProcess),
+ coordinate_loop_secondary()
+ end
end),
receive
{Coordinator, ready} ->
io:fwrite("[Automate storage] Ready~n"),
- {ok, Coordinator}
+ {ok, Coordinator};
+ {shutdown,_}=Shutdown ->
+ exit(Shutdown)
end.
%% Not a primary node
@@ -1319,6 +2443,7 @@ wait_for_all_nodes_ready(false, Primary, NonPrimaries) ->
io:fwrite("~p ! ~p~n", [{?SERVER, Primary}, { self(), {node_ready, node() }}]),
receive
{ _From, storage_started } ->
+ io:fwrite("[~p:~p] Node storage started confirmed~n", [?MODULE, ?LINE]),
ok;
X ->
io:fwrite("[automate_storage coordinator | ~p | Prim: ~p] Unknown message: ~p~n",
@@ -1351,16 +2476,100 @@ wait_for_all_nodes_ready(true, Primary, NonPrimariesToGo) ->
io:fwrite("[automate_storage coordinator | Prim, ~p] Unknown message: ~p~n",
[node(), X]),
wait_for_all_nodes_ready(true, Primary, NonPrimariesToGo)
+
+ after ?WAIT_READY_LOOP_TIME ->
+ lists:foreach(fun(Secondary) ->
+ {?SERVER, Secondary} ! { self(), {primary_waiting, node()} }
+ end, sets:to_list(NonPrimariesToGo)),
+ wait_for_all_nodes_ready(true, Primary, NonPrimariesToGo)
end
end.
-coordinate_loop(Primary) ->
+-spec coordinate_secondary_loop_wait_for_primary_and_crash() -> no_return().
+coordinate_secondary_loop_wait_for_primary_and_crash() ->
receive
- %% To be defined
+ {_From, {primary_waiting, Node}} ->
+ io:fwrite("[~p:~p] Primary node (~p) waiting. Stopping secondary node ~p~n",
+ [?MODULE, ?LINE, Node, node()]),
+
+ %% Mark the node as partially restarted
+ true = ets:insert(?ETS_TABLE_SECONDARY_NODE_RESTART, [ { partial_restart, true } ]),
+ exit(primary_disconnected);
X ->
- io:fwrite("[automate_storage coordinator | ~p | Prim: ~p] Unknown message: ~p~n",
- [node(), Primary, X]),
- coordinate_loop(Primary)
+ io:fwrite("[~p:~p][Secondary coordinator waiting for primary to go back up | ~p] Unknown message: ~p~n",
+ [?MODULE, ?LINE, node(), X]),
+ coordinate_secondary_loop_wait()
+ end.
+
+-spec coordinate_secondary_loop_wait() -> no_return().
+coordinate_secondary_loop_wait() ->
+ receive
+ {'DOWN', _MonitorRef, process, _Object, _Info} ->
+ io:fwrite("[~p:~p] Primary node failed. Waiting for primary before stopping secondary node ~p~n",
+ [?MODULE, ?LINE, node()]),
+ %% Wait for primary to come back up, and exit
+ coordinate_secondary_loop_wait_for_primary_and_crash();
+ X ->
+ io:fwrite("[~p:~p][Secondary coordinator | ~p] Unknown message: ~p~n",
+ [?MODULE, ?LINE, node(), X]),
+ coordinate_secondary_loop_wait()
+ end.
+
+-spec coordinate_loop_secondary() -> no_return().
+coordinate_loop_secondary() ->
+ %% Prepare a table to be used to store data between processes of automate_storage.
+ case ets:whereis(?ETS_TABLE_SECONDARY_NODE_RESTART) of
+ undefined ->
+ ets:new(?ETS_TABLE_SECONDARY_NODE_RESTART, [ named_table, public, set, { heir, whereis(automate_sup), 'process-metadata' } ] );
+ _ ->
+ ok
+ end,
+
+ case ets:lookup(?ETS_TABLE_SECONDARY_NODE_RESTART, partial_restart) of
+ [{ partial_restart, true }] ->
+ %% Crash the node to restart completely
+ ets:delete(?ETS_TABLE_SECONDARY_NODE_RESTART),
+ erlang:halt();
+ [] ->
+ %% Everything is alright
+ ok
+ end,
+
+ coordinate_secondary_loop_wait().
+
+-spec coordinate_loop_primary() -> no_return().
+coordinate_loop_primary() ->
+ receive
+ { From, { node_ready, Node } } ->
+ io:fwrite("[~p:~p] Merging diverged node: ~p~n", [?MODULE, ?LINE, Node]),
+ ok = add_mnesia_node(Node),
+ From ! {self(), storage_started},
+ coordinate_loop_primary();
+
+ {mnesia_system_event, {mnesia_fatal, Format, Args, BinaryCore}} ->
+ io:fwrite("[~p:~p] Fatal error in mnesia:~n ~p~n", [?MODULE, ?LINE, {Format, Args, BinaryCore}]),
+ coordinate_loop_primary();
+
+ {mnesia_system_event, {mnesia_down, Node}} ->
+ io:fwrite("[~p:~p] Mnesia node down: ~p~n", [?MODULE, ?LINE, Node]),
+ coordinate_loop_primary();
+
+ {mnesia_system_event, {mnesia_up, Node}} ->
+ io:fwrite("[~p:~p] Mnesia node up: ~p~n", [?MODULE, ?LINE, Node]),
+ coordinate_loop_primary();
+
+ {mnesia_system_event, {mnesia_info, Format, Args}} ->
+ io:fwrite("[~p:~p] " ++ Format, [?MODULE | [ ?LINE | Args ]]),
+ coordinate_loop_primary();
+
+ {mnesia_system_event, {inconsistent_database, Context, Node}} ->
+ io:fwrite("[~p:~p] Mnesia inconsistent database:~n ~p~n", [?MODULE, ?LINE, {Context, Node}]),
+ coordinate_loop_primary();
+
+ X ->
+ io:fwrite("[~p:~p][Primary coordinator | ~p] Unknown message: ~p~n",
+ [?MODULE, ?LINE, node(), X]),
+ coordinate_loop_primary()
end.
prepare_nodes(Nodes) ->
@@ -1381,3 +2590,30 @@ build_tables(Nodes) ->
generate_id() ->
binary:list_to_bin(uuid:to_string(uuid:uuid4())).
+
+
+-spec token_scope_covers(TokenScope :: session_scope(), Scope :: session_scope_item()) -> boolean().
+token_scope_covers(all, _) -> true;
+token_scope_covers(TokenScope, Scope) when is_list(TokenScope) ->
+ %% Look for a direct match
+ case lists:member(Scope, TokenScope) of
+ true ->
+ true;
+ false ->
+ lists:any(fun(TokenScopeItem) ->
+ token_scope_covers_by_higher_level(TokenScopeItem, Scope)
+ end, TokenScope)
+ end.
+
+-spec token_scope_covers_by_higher_level(TokenScopeItem :: session_scope_item(), Scope :: session_scope_item()) -> boolean().
+
+%% Full permissions over bridges user's own bridges
+token_scope_covers_by_higher_level(call_any_bridge, { call_bridge, _, _ }) -> true;
+token_scope_covers_by_higher_level(call_any_bridge, { call_bridge_callback, _ }) -> true;
+token_scope_covers_by_higher_level(call_any_bridge, { call_bridge_callback, _, _ }) -> true;
+
+%% Any is ok for check
+token_scope_covers_by_higher_level(_, check) -> true;
+
+%% No match
+token_scope_covers_by_higher_level(_, _) -> false.
diff --git a/backend/apps/automate_storage/src/automate_storage_app.erl b/backend/apps/automate_storage/src/automate_storage_app.erl
index be6cb3ac..ef710df1 100644
--- a/backend/apps/automate_storage/src/automate_storage_app.erl
+++ b/backend/apps/automate_storage/src/automate_storage_app.erl
@@ -8,14 +8,16 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
+start() ->
+ automate_storage_sup:start_link().
start(_StartType, _StartArgs) ->
- automate_storage_sup:start_link().
+ start().
%%--------------------------------------------------------------------
stop(_State) ->
diff --git a/backend/apps/automate_storage/src/automate_storage_configuration.erl b/backend/apps/automate_storage/src/automate_storage_configuration.erl
index ffdfac15..9ab9c935 100644
--- a/backend/apps/automate_storage/src/automate_storage_configuration.erl
+++ b/backend/apps/automate_storage/src/automate_storage_configuration.erl
@@ -6,6 +6,7 @@
-module(automate_storage_configuration).
-export([ get_versioning/1
+ , db_map/2
]).
-include("./databases.hrl").
@@ -83,14 +84,14 @@ get_versioning(Nodes) ->
%%
{ id=1
, apply=fun() ->
- automate_storage_versioning:create_database(
- #database_version_data
- { database_name=?INSTALLATION_CONFIGURATION_TABLE
- , records=[ id
- , value
- ]
- , record_name=storage_configuration_entry
- }, Nodes),
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?INSTALLATION_CONFIGURATION_TABLE
+ , records=[ id
+ , value
+ ]
+ , record_name=storage_configuration_entry
+ }, Nodes),
ok = mnesia:wait_for_tables([ ?INSTALLATION_CONFIGURATION_TABLE ],
automate_configuration:get_table_wait_time())
@@ -104,17 +105,17 @@ get_versioning(Nodes) ->
, #database_version_transformation
{ id=2
, apply=fun() ->
- mnesia:transform_table(
- ?USER_PROGRAMS_TABLE,
- fun({user_program_entry, Id, UserId, ProgramName,
- ProgramType, ProgramParsed, ProgramOrig }) ->
- %% Replicate the entry. Just set enabled to true.
- {user_program_entry, Id, UserId, ProgramName,
- ProgramType, ProgramParsed, ProgramOrig, true }
- end,
- [ id, user_id, program_name, program_type, program_parsed, program_orig, enabled],
- user_program_entry
- )
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_PROGRAMS_TABLE,
+ fun({user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig }) ->
+ %% Replicate the entry. Just set enabled to true.
+ {user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, true }
+ end,
+ [ id, user_id, program_name, program_type, program_parsed, program_orig, enabled],
+ user_program_entry
+ )
end
}
@@ -124,15 +125,15 @@ get_versioning(Nodes) ->
, #database_version_transformation
{ id=3
, apply=fun() ->
- automate_storage_versioning:create_database(
- #database_version_data
- { database_name=?CUSTOM_SIGNALS_TABLE
- , records=[ id
- , name
- , owner
- ]
- , record_name=custom_signal_entry
- }, Nodes),
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?CUSTOM_SIGNALS_TABLE
+ , records=[ id
+ , name
+ , owner
+ ]
+ , record_name=custom_signal_entry
+ }, Nodes),
ok = mnesia:wait_for_tables([ ?CUSTOM_SIGNALS_TABLE ],
automate_configuration:get_table_wait_time())
@@ -145,34 +146,32 @@ get_versioning(Nodes) ->
, #database_version_transformation
{ id=4
, apply=fun() ->
- mnesia:transform_table(
- ?REGISTERED_USERS_TABLE,
- fun({registered_user_entry, Id, Username, Password, Email }) ->
- %% Replicate the entry. Set status to ready.
- {registered_user_entry, Id, Username, Password, Email,
- ready }
- end,
- [ id, username, password, email, status ],
- registered_user_entry
- )
+ {atomic, ok} = mnesia:transform_table(
+ ?REGISTERED_USERS_TABLE,
+ fun({registered_user_entry, Id, Username, Password, Email }) ->
+ %% Replicate the entry. Set status to ready.
+ {registered_user_entry, Id, Username, Password, Email,
+ ready }
+ end,
+ [ id, username, password, email, status ],
+ registered_user_entry
+ )
end
}
- %% Add *status* to user table.
- %%
- %% If a user "comes" from an earlier version the status is 'ready'.
+ %% Create user verification table.
, #database_version_transformation
{ id=5
, apply=fun() ->
- automate_storage_versioning:create_database(
- #database_version_data
- { database_name=?USER_VERIFICATION_TABLE
- , records=[ id
- , user_id
- , verification_type
- ]
- , record_name=user_verification_entry
- }, Nodes),
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_VERIFICATION_TABLE
+ , records=[ id
+ , user_id
+ , verification_type
+ ]
+ , record_name=user_verification_entry
+ }, Nodes),
ok = mnesia:wait_for_tables([ ?USER_VERIFICATION_TABLE ],
automate_configuration:get_table_wait_time())
@@ -189,7 +188,7 @@ get_versioning(Nodes) ->
mnesia:transform_table(
?REGISTERED_USERS_TABLE,
fun({registered_user_entry, Id, Username, Password, Email, Status }) ->
- %% Replicate the entry. Set status to ready.
+ %% Replicate the entry. Set registration time to unknown.
{registered_user_entry, Id, Username, Password, Email,
Status, 0 }
end,
@@ -206,17 +205,706 @@ get_versioning(Nodes) ->
, #database_version_transformation
{ id=7
, apply=fun() ->
- mnesia:transform_table(
- ?USER_SESSIONS_TABLE,
- fun({user_session_entry, SessionId, UserId, SessionStartTime }) ->
- %% Replicate the entry. Set status to ready.
- {user_session_entry, SessionId, UserId, SessionStartTime,
- 0 }
- end,
- [ session_id, user_id, session_start_time, session_last_used_time ],
- user_session_entry
- )
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_SESSIONS_TABLE,
+ fun({user_session_entry, SessionId, UserId, SessionStartTime }) ->
+ %% Replicate the entry. Set session_last_used_time to unknown.
+ {user_session_entry, SessionId, UserId, SessionStartTime,
+ 0 }
+ end,
+ [ session_id, user_id, session_start_time, session_last_used_time ],
+ user_session_entry
+ )
+ end
+ }
+
+ %% Add user program logs table.
+ , #database_version_transformation
+ { id=8
+ , apply=fun() ->
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_PROGRAM_LOGS_TABLE
+ , records=[ program_id
+ , thread_id
+ , user_id
+ , block_id
+ , event_data
+ , event_message
+ , event_time
+ , severity
+ , exception_data
+ ]
+ , record_name=user_program_log_entry
+ , type=bag
+ }, Nodes),
+
+ ok = mnesia:wait_for_tables([ ?USER_PROGRAM_LOGS_TABLE ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+
+ %% Add `update_channel` entry to programs table.
+ %%
+ %% This is used to stream the changes happening on the programs.
+ %% The channel will be deleted when the program is.
+ , #database_version_transformation
+ { id=9
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ automate_user_programs, %% ?USER_PROGRAMS_TABLE
+ fun({user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, Enabled }) ->
+ %% Replicate the entry. Just create an empty program channel.
+
+ { user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, Enabled,
+ undefined }
+ end,
+ [ id, user_id, program_name, program_type, program_parsed, program_orig, enabled
+ , program_channel
+ ],
+ user_program_entry
+ ),
+
+ %% After the table is updated, generate the new channels
+ %% This apparently cannot be done inside the mnesia:transform_table.
+ ok = db_map(automate_user_programs, %% ?USER_PROGRAMS_TABLE
+ fun({user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, Enabled, ProgramChannel }) ->
+ NewChannel = case ProgramChannel of
+ undefined ->
+ {ok, CreatedChannel} = automate_channel_engine:create_channel(),
+ io:fwrite("Created channel: ~p~n", [CreatedChannel]),
+ CreatedChannel;
+ _ ->
+ ProgramChannel
+ end,
+ {user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, Enabled, NewChannel }
+ end)
+ end
+ , revert=fun() ->
+ %% Before the table is updated, remove the old channels
+ %% This apparently cannot be done inside the mnesia:transform_table.
+ ok = db_map(automate_user_programs, %% ?USER_PROGRAMS_TABLE
+ fun({user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, Enabled, ProgramChannel }) ->
+ case ProgramChannel of
+ undefined ->
+ ok;
+ _ ->
+ %% Replicate the entry. Just create a program channel.
+
+ Result = automate_channel_engine:delete_channel(ProgramChannel),
+ io:fwrite("Deleting channel ~p: ~p~n", [ProgramChannel, Result])
+ end,
+ { user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, Enabled }
+ end),
+
+ {atomic, ok} = mnesia:transform_table(
+ automate_user_programs, %% ?USER_PROGRAMS_TABLE
+ fun({user_program_entry, Id, UserId, ProgramName
+ , ProgramType, ProgramParsed, ProgramOrig, Enabled
+ , _ProgramChannel }) ->
+ { user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig, Enabled }
+ end,
+ [ id, user_id, program_name, program_type, program_parsed, program_orig, enabled
+ ],
+ user_program_entry
+ )
+ end
+ }
+
+ %% Add stability metrics to programs table.
+ %%
+ %% - creation_time : To put the rest in context
+ %% - last_upload_time : To know how "fresh" the program is
+ %% - last_successful_call_time : Relative to the next metric puts a number on the current health of the program
+ %% - last_failed_call_time : Relative to the previous metric puts a number on the current health of the program
+ %%
+ , #database_version_transformation
+ { id=10
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ automate_user_programs, %% ?USER_PROGRAMS_TABLE
+ fun({user_program_entry, Id, UserId, ProgramName,
+ ProgramType, ProgramParsed, ProgramOrig,
+ Enabled, ProgramChannel }) ->
+ %% Replicate the entry. Fill unknown times with '0'
+
+ { user_program_entry, Id, UserId, ProgramName
+ , ProgramType, ProgramParsed, ProgramOrig, Enabled, ProgramChannel
+ , 0, 0, 0, 0
+ }
+
+ end,
+ [ id, user_id, program_name, program_type, program_parsed, program_orig, enabled, program_channel
+ , creation_time, last_upload_time, last_successful_call_time, last_failed_call_time
+ ],
+ user_program_entry
+ )
+ end
+ }
+
+ %% - Add user tags `is_admin`, `is_advanced`, `is_in_preview` to user table.
+ %%
+ %% Previous records are set to `false`.
+ , #database_version_transformation
+ { id=11
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?REGISTERED_USERS_TABLE,
+ fun({registered_user_entry, Id, Username, Password, Email, Status, RegistrationTime }) ->
+ %% Replicate the entry. Set is_admin/advanced/in_preview to false.
+ { registered_user_entry, Id, Username, Password, Email, Status, RegistrationTime
+ , false, false, false
+ }
+ end,
+ [ id, username, password, email, status, registration_time
+ , is_admin, is_advanced, is_in_preview
+ ],
+ registered_user_entry
+ )
+ end
+ }
+
+ %% - Add `canonical_username` to user table.
+ %%
+ %% Previous records are set to lowercased usernames, but they might not be correct usernames.
+ , #database_version_transformation
+ { id=12
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?REGISTERED_USERS_TABLE,
+ fun({registered_user_entry, Id, Username, Password
+ , Email, Status, RegistrationTime
+ , IsAdmin, IsAdvanced, IsInPreview
+ }) ->
+ CanonicalUsername = automate_storage_utils:canonicalize(Username),
+
+ %% Replicate the entry. Set canonicalized username.
+ { registered_user_entry, Id, Username, CanonicalUsername, Password
+ , Email, Status, RegistrationTime
+ , IsAdmin, IsAdvanced, IsInPreview
+ }
+ end,
+ [ id, username, canonical_username, password, email, status, registration_time
+ , is_admin, is_advanced, is_in_preview
+ ],
+ registered_user_entry
+ )
+ end
+ }
+
+ %% Add user generated logs table.
+ , #database_version_transformation
+ { id=13
+ , apply=fun() ->
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_GENERATED_LOGS_TABLE
+ , records=[ program_id
+ , block_id
+ , severity
+ , event_time
+ , event_message
+ ]
+ , record_name=user_generated_log_entry
+ , type=bag
+ }, Nodes),
+
+ ok = mnesia:wait_for_tables([ ?USER_GENERATED_LOGS_TABLE ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+
+ %% Add user editor events table.
+ , #database_version_transformation
+ { id=14
+ , apply=fun() ->
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_PROGRAM_EVENTS_TABLE
+ , records=[ program_id
+ , event
+ , event_tag
+ ]
+ , record_name=user_program_editor_event
+ , type=bag
+ }, Nodes),
+
+ ok = mnesia:wait_for_tables([ ?USER_PROGRAM_EVENTS_TABLE ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+
+ %% Add program checkpoints table.
+ , #database_version_transformation
+ { id=15
+ , apply=fun() ->
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_PROGRAM_CHECKPOINTS_TABLE
+ , records=[ program_id
+ , user_id
+ , event_time
+ , content
+ ]
+ , record_name=user_program_checkpoint
+ , type=bag
+ }, Nodes),
+
+ ok = mnesia:wait_for_tables([ ?USER_PROGRAM_CHECKPOINTS_TABLE ],
+ automate_configuration:get_table_wait_time())
end
}
+
+ %% Introduce user groups
+ , #database_version_transformation
+ { id=16
+ , apply=fun() ->
+ %% This table might be problematic, so force to load it here
+ ok = automate_storage_maintenance:wait_table(?USER_PROGRAM_LOGS_TABLE),
+
+ ok = automate_storage_versioning:create_database(
+ #database_version_data
+ { database_name=?USER_GROUPS_TABLE
+ , records=[ id
+ , name
+ ]
+ , record_name=user_group_entry
+ , type=set
+ }, Nodes),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_PROGRAM_LOGS_TABLE,
+ fun({ user_program_log_entry
+ , ProgramId, ThreadId, UserId, BlockId, EventData, EventMessage
+ , EventTime, Severity, ExceptionData
+ }) ->
+ { user_program_log_entry
+ , ProgramId, ThreadId, {user, UserId}, BlockId, EventData, EventMessage
+ , EventTime, Severity, ExceptionData
+ }
+ end,
+ [ program_id, thread_id, owner, block_id, event_data
+ , event_message, event_time, severity, exception_data
+ ],
+ user_program_log_entry
+ ),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_PROGRAMS_TABLE,
+ fun({ user_program_entry
+ , Id, UserId, ProgramName, ProgramType
+ , ProgramParsed, ProgramOrig, Enabled, ProgramChannel
+ , CreationTime, LastUploadTime, LastSuccessfulCalltime
+ , LastFailedCallTime
+ }) ->
+ { user_program_entry
+ , Id, {user, UserId}, ProgramName, ProgramType
+ , ProgramParsed, ProgramOrig, Enabled, ProgramChannel
+ , CreationTime, LastUploadTime, LastSuccessfulCalltime
+ , LastFailedCallTime
+ }
+ end,
+ [ id, owner, program_name, program_type, program_parsed, program_orig, enabled, program_channel
+ , creation_time, last_upload_time, last_successful_call_time, last_failed_call_time
+ ],
+ user_program_entry
+ ),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?CUSTOM_SIGNALS_TABLE,
+ fun({ custom_signal_entry
+ , Id, Name, Owner
+ }) ->
+ { custom_signal_entry
+ , Id, Name, {user, Owner}
+ }
+ end,
+ [ id, name, owner
+ ],
+ custom_signal_entry
+ ),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_MONITORS_TABLE,
+ fun({ monitor_entry
+ , Id, UserId, Type, Name, Value
+ }) ->
+ { monitor_entry
+ , Id, {user, UserId}, Type, Name, Value
+ }
+ end,
+ [ id, owner, type, name, value
+ ],
+ monitor_entry
+ ),
+
+ ok = mnesia:wait_for_tables([ ?USER_GROUPS_TABLE, ?USER_PROGRAM_LOGS_TABLE
+ , ?USER_PROGRAMS_TABLE, ?CUSTOM_SIGNALS_TABLE, ?USER_MONITORS_TABLE
+ ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+
+ %% Add user groups permissions
+ , #database_version_transformation
+ { id=17
+ , apply=fun() ->
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_GROUPS_TABLE,
+ fun( {user_group_entry, Id, Name} ) ->
+ { user_group_entry, Id, Name, automate_storage_utils:canonicalize(Name), false }
+ end,
+ [ id, name, canonical_name, public ],
+ user_group_entry),
+
+ {atomic, ok} = mnesia:add_table_index(?USER_GROUPS_TABLE, canonical_name),
+ {atomic, ok} = mnesia:add_table_index(?USER_PROGRAMS_TABLE, owner),
+ {atomic, ok} = mnesia:add_table_index(?USER_MONITORS_TABLE, owner),
+
+ {atomic, ok} = mnesia:create_table(?USER_GROUP_PERMISSIONS_TABLE,
+ [ { attributes, [group_id, user_id, role] }
+ , { disc_copies, Nodes }
+ , { record_name, user_group_permissions_entry }
+ , { type, bag }
+ , { index, [ user_id ] }
+ ]),
+
+ ok = mnesia:wait_for_tables([ ?USER_GROUP_PERMISSIONS_TABLE
+ ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+
+ %% Add groups creation time
+ , #database_version_transformation
+ { id=18
+ , apply=fun() ->
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_GROUPS_TABLE,
+ fun( {user_group_entry, Id, Name, CanonicalName, Public} ) ->
+ {user_group_entry, Id, Name, CanonicalName, Public, 0}
+ end,
+ [ id, name, canonical_name, public, creation_time ],
+ user_group_entry
+ )
+ end
+ }
+
+ %% Add groups creation time
+ , #database_version_transformation
+ { id=19
+ , apply=fun() ->
+
+ {atomic, ok} = mnesia:create_table(?PROGRAM_PAGES_TABLE,
+ [ { attributes, [ page_id, program_id, contents ] }
+ , { disc_copies, Nodes }
+ , { record_name, program_pages_entry }
+ , { type, set }
+ , { index, [ program_id ] }
+ ]),
+
+ ok = mnesia:wait_for_tables([ ?PROGRAM_PAGES_TABLE
+ ],
+ automate_configuration:get_table_wait_time())
+
+ end
+ }
+
+ %% Add widget's last value table
+ , #database_version_transformation
+ { id=20
+ , apply=fun() ->
+
+ {atomic, ok} = mnesia:create_table(?PROGRAM_WIDGET_VALUE_TABLE,
+ [ { attributes, [ widget_id, program_id, value ] }
+ , { disc_copies, Nodes }
+ , { record_name, program_widget_value_entry }
+ , { type, set }
+ , { index, [ program_id ] }
+ ]),
+
+ ok = mnesia:wait_for_tables([ ?PROGRAM_WIDGET_VALUE_TABLE
+ ],
+ automate_configuration:get_table_wait_time())
+
+ end
+ }
+
+ %% Add asset table time and re-structure the asset directories
+ , #database_version_transformation
+ { id=21
+ , apply=fun() ->
+
+ {atomic, ok} = mnesia:create_table(?USER_ASSET_TABLE,
+ [ { attributes, [ asset_id, owner_id, mime_type ] }
+ , { disc_copies, Nodes }
+ , { record_name, user_asset_entry }
+ , { type, set }
+ , { index, [ owner_id ] }
+ ]),
+
+ ok = mnesia:wait_for_tables([ ?USER_ASSET_TABLE
+ ],
+ automate_configuration:get_table_wait_time()),
+
+ %% Update user/group picture paths
+ {atomic, ok} = mnesia:transaction(
+ fun() ->
+ Migrate = fun(Tab, AssetDirectory) ->
+ G = mnesia:all_keys(Tab),
+ io:fwrite("~p ~p~n", [length(G), AssetDirectory]),
+
+ GetOldPath = fun(Id) ->
+ list_to_binary([ automate_configuration:asset_directory(list_to_binary(["public/", AssetDirectory, "/"]))
+ , "/", Id
+ ])
+ end,
+
+ HasPicture = (
+ fun(Id) ->
+ Path = GetOldPath(Id),
+ filelib:is_regular(Path)
+ end),
+
+ WithPicture = lists:filter(HasPicture, G),
+ io:fwrite("~p ~p with old picture~n", [length(WithPicture), AssetDirectory]),
+
+ ok = lists:foreach(
+ fun(Id) ->
+ io:fwrite("Updating ~p~n", [Id]),
+ OldPath = GetOldPath(Id),
+ TmpPath = list_to_binary([OldPath, ".tmp"]),
+ NewPath = list_to_binary([OldPath, "/picture"]),
+
+ ok = file:rename(OldPath, TmpPath),
+ ok = filelib:ensure_dir(NewPath),
+ ok = file:rename(TmpPath, NewPath)
+
+ end, WithPicture)
+ end,
+ ok = Migrate(?REGISTERED_USERS_TABLE, "users"),
+ ok = Migrate(?USER_GROUPS_TABLE, "groups")
+ end)
+ end
+ }
+
+ %% Add `creation_time` and `used` fields to user verification entries.
+ %%
+ %% This should allow to keep it for longer to avoid showing errors when applied twice.
+ , #database_version_transformation
+ { id=22
+ , apply=fun() ->
+ CurrentTime = erlang:system_time(second),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_VERIFICATION_TABLE,
+ fun({user_verification_entry, Id, UserId, VerificationType }) ->
+ %% Replicate the entry. Set Creation and used to { current_time(), false }.
+ { user_verification_entry, Id, UserId, VerificationType
+ , CurrentTime, false }
+ end,
+ [ id, user_id, verification_type, creation_time, used ],
+ user_verification_entry
+ )
+ end
+ }
+
+ %% Add visibility to user programs
+ , #database_version_transformation
+ { id=23
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_PROGRAMS_TABLE,
+ fun({ user_program_entry
+ , Id, UserId, ProgramName, ProgramType
+ , ProgramParsed, ProgramOrig, Enabled, ProgramChannel
+ , CreationTime, LastUploadTime, LastSuccessfulCalltime
+ , LastFailedCallTime
+ }) ->
+ { user_program_entry
+ , Id, UserId, ProgramName, ProgramType
+ , ProgramParsed, ProgramOrig, Enabled, ProgramChannel
+ , CreationTime, LastUploadTime, LastSuccessfulCalltime
+ , LastFailedCallTime
+ , false %% Default to non-public
+ }
+ end,
+ [ id, owner, program_name, program_type, program_parsed, program_orig, enabled, program_channel
+ , creation_time, last_upload_time, last_successful_call_time, last_failed_call_time
+ , is_public
+ ],
+ user_program_entry
+ )
+ end
+ }
+
+ %% Add profile configuration
+ , #database_version_transformation
+ { id=24
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_PROGRAMS_TABLE,
+ fun({ user_program_entry
+ , Id, UserId, ProgramName, ProgramType
+ , ProgramParsed, ProgramOrig, Enabled, ProgramChannel
+ , CreationTime, LastUploadTime, LastSuccessfulCalltime
+ , LastFailedCallTime, IsPublic
+ }) ->
+ %% Translate `is_public` to `visibility`
+ Visibility = case IsPublic of
+ true -> shareable;
+ false -> private
+ end,
+
+ { user_program_entry
+ , Id, UserId, ProgramName, ProgramType
+ , ProgramParsed, ProgramOrig, Enabled, ProgramChannel
+ , CreationTime, LastUploadTime, LastSuccessfulCalltime
+ , LastFailedCallTime
+ , Visibility
+ }
+ end,
+ [ id, owner, program_name, program_type, program_parsed, program_orig, enabled, program_channel
+ , creation_time, last_upload_time, last_successful_call_time, last_failed_call_time
+ , visibility
+ ],
+ user_program_entry
+ ),
+
+ {atomic, ok} = mnesia:create_table(?USER_PROFILE_LISTINGS_TABLE,
+ [ { attributes, [ id, groups ] }
+ , { disc_copies, Nodes }
+ , { record_name, user_profile_listings_entry }
+ , { type, set }
+ , { index, [ ] }
+ ]),
+
+ ok = mnesia:wait_for_tables([ ?USER_PROFILE_LISTINGS_TABLE
+ ],
+ automate_configuration:get_table_wait_time())
+ end
+ }
+
+ %% Add direction to program threads
+ , #database_version_transformation
+ { id=25
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?RUNNING_THREADS_TABLE,
+ fun({ running_program_thread_entry
+ , ThreadId, RunnerPid, ParentProgramId
+ , Instructions, Memory, InstructionMemory
+ , Position, Stats
+ }) ->
+ { running_program_thread_entry
+ , ThreadId, RunnerPid, ParentProgramId
+ , Instructions, Memory, InstructionMemory
+ , Position, Stats
+ , forward
+ }
+ end,
+ [ thread_id, runner_pid, parent_program_id
+ , instructions, memory, instruction_memory
+ , position, stats, direction
+ ],
+ running_program_thread_entry
+ )
+ end
+ }
+
+ %% Add `min.level to privately use bridge` to group entry.
+ %% Default to `not_allowed`.
+ , #database_version_transformation
+ { id=26
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_GROUPS_TABLE,
+ fun( {user_group_entry, Id, Name, CanonicalName, Public, CreationTime} ) ->
+ {user_group_entry, Id, Name, CanonicalName, Public, CreationTime, not_allowed}
+ end,
+ [ id, name, canonical_name, public, creation_time, min_level_for_private_bridge_usage ],
+ user_group_entry
+ )
+ end
+ }
+
+ %% Index program variables by ProgramId.
+ , #database_version_transformation
+ { id=27
+ , apply=fun() ->
+
+ ok = mnesia:wait_for_tables([ ?PROGRAM_VARIABLE_TABLE
+ ],
+ automate_configuration:get_table_wait_time()),
+
+ {atomic, ok} = mnesia:transform_table(
+ ?PROGRAM_VARIABLE_TABLE,
+ fun( {program_variable_table_entry, {ProgramId, VarName}, Value} ) ->
+ {program_variable_table_entry, {ProgramId, VarName}, ProgramId, Value}
+ end,
+ [ id, program_id, value ],
+ program_variable_table_entry
+ ),
+
+ {atomic, ok} = mnesia:add_table_index(?PROGRAM_VARIABLE_TABLE, program_id)
+ end
+ }
+
+ %% - Add scopes to user tokens
+ %% - Add expiration date to user tokens
+ %%
+ %% Previous tokens are set to scopes=all, expiration_time=session
+ , #database_version_transformation
+ { id=28
+ , apply=fun() ->
+ ok = mnesia:wait_for_tables([ ?USER_SESSIONS_TABLE ],
+ automate_configuration:get_table_wait_time()),
+ {atomic, ok} = mnesia:transform_table(
+ ?USER_SESSIONS_TABLE,
+ fun({user_session_entry, SessionId, UserId, SessionStartTime, SessionLastUsedTime }) ->
+ %% Replicate the entry. Set session_last_used_time to unknown.
+ { user_session_entry, SessionId, UserId, SessionStartTime, SessionLastUsedTime
+ , all %% Scopes
+ , session %% Expiration time
+ }
+ end,
+ [ session_id, user_id, session_start_time, session_last_used_time
+ , session_scope, session_expiration_time ],
+ user_session_entry
+ )
+ end
+ }
+
]
}.
+
+db_map(Database, Function) ->
+ Transaction = fun() ->
+ ok = mnesia:write_lock_table(Database),
+ ok = db_map_iter(Database, Function, mnesia:first(Database))
+ end,
+ case mnesia:transaction(Transaction) of
+ {atomic, Result} ->
+ Result;
+ {aborted, Reason} ->
+ io:fwrite("[Storage/Migration] Error on migration: ~p~n", [Reason]),
+ {error, Reason}
+ end.
+
+db_map_iter(_Database, _Function, '$end_of_table') ->
+ ok;
+db_map_iter(Database, Function, Key) ->
+ [Element] = mnesia:read(Database, Key),
+ NewElement = Function(Element),
+ %% Note that the old element is not removed. An update cannot change the ID.
+ ok = mnesia:write(Database, NewElement, write),
+ db_map_iter(Database, Function, mnesia:next(Database, Key)).
diff --git a/backend/apps/automate_storage/src/automate_storage_maintenance.erl b/backend/apps/automate_storage/src/automate_storage_maintenance.erl
new file mode 100644
index 00000000..29e3d2b2
--- /dev/null
+++ b/backend/apps/automate_storage/src/automate_storage_maintenance.erl
@@ -0,0 +1,138 @@
+-module(automate_storage_maintenance).
+
+%% API exports
+-export([ wait_table/1
+ , prune_user_program_logs/0
+ , get_db_status/0
+ , flush_disc_table/1
+ , with_backup/1
+ ]).
+
+-include("./databases.hrl").
+-include("./records.hrl").
+
+-define(FORCE_LOADING_SLEEP_TIME, 10000).
+-define(MAX_LOAD_TABLE, 1000 * 60 * 60 * 6). %% 6 Hours
+-define(MAX_PROGRAM_LOGS, 1000).
+
+%%====================================================================
+%% API functions
+%%====================================================================
+prune_user_program_logs() ->
+ ok = wait_table(?USER_PROGRAM_LOGS_TABLE),
+ {atomic, ok} = mnesia:transaction(fun() ->
+ ok = mnesia:write_lock_table(?USER_PROGRAM_LOGS_TABLE),
+ Keys = mnesia:all_keys(?USER_PROGRAM_LOGS_TABLE),
+ lists:foreach(fun(K) ->
+ Elements = mnesia:read(?USER_PROGRAM_LOGS_TABLE, K),
+ case length(Elements) > ?MAX_PROGRAM_LOGS of
+ true ->
+ Sorted = lists:sort(fun( #user_program_log_entry{ event_time=Time1 }
+ , #user_program_log_entry{ event_time=Time2 }
+ ) ->
+ Time1 >= Time2
+ end, Elements),
+ {Kept, _} = lists:split(?MAX_PROGRAM_LOGS + 1, Sorted),
+
+ %% Delete old values
+ mnesia:delete(?USER_PROGRAM_LOGS_TABLE, K, write),
+
+ %% Write new values
+ lists:foreach(fun(Element) ->
+ ok = mnesia:write(?USER_PROGRAM_LOGS_TABLE, Element, write)
+ end, Kept);
+
+ %% If the limit of logs was not exceeded, do not make any change
+ _ -> ok
+ end
+ end, Keys)
+ end),
+ flush_disc_table(?USER_PROGRAM_LOGS_TABLE).
+
+get_db_status() ->
+ Tables = mnesia:system_info(tables),
+ WordSize = erlang:system_info(wordsize),
+
+ TableInfo = lists:map(fun(Tab) ->
+ Size = mnesia:table_info(Tab, size),
+ Ready = length(mnesia:table_info(Tab, active_replicas)) > 0,
+ Memory = mnesia:table_info(Tab, memory) * WordSize,
+
+ {Tab, [ { ready, Ready }
+ , { size, Size }
+ , { memory_kb, Memory / 1024 }
+ ]}
+ end, Tables),
+ NonReadyTables = lists:filtermap(fun({Tab, TabInfo}) ->
+ case proplists:get_value(ready, TabInfo) of
+ true -> false;
+ false -> { true, Tab }
+ end
+ end, TableInfo),
+ [{ tables, TableInfo }, {non_ready, NonReadyTables}].
+
+wait_table(Tab) ->
+ Orig = self(),
+ {_Pid, Ref} = spawn_monitor(fun() ->
+ Orig ! {load_table, mnesia:wait_for_tables([Tab], ?MAX_LOAD_TABLE)}
+ end),
+ wait_table_loading(Tab, Ref).
+
+flush_disc_table(Tab) ->
+ with_backup(fun() ->
+ ok = lists:foreach(fun(Node) ->
+ io:fwrite("Flushing node: ~p...", [Node]),
+ mnesia:change_table_copy_type(Tab, Node, ram_copies),
+ mnesia:change_table_copy_type(Tab, Node, disc_copies),
+ io:fwrite(" ok!~n")
+ end, mnesia:table_info(Tab, active_replicas))
+ end).
+
+with_backup(Fun) ->
+ BackupName = "mnesia_" ++ integer_to_list(erlang:phash2(make_ref())),
+ BackupDir = filename:basedir(user_cache, "automate"),
+ BackupPath = BackupDir ++ "/" ++ BackupName,
+ io:fwrite("Backing up to ~p~n", [BackupPath]),
+
+ ok = filelib:ensure_dir(BackupPath), %% Yes, this is applied to the full path
+
+ ok = mnesia:backup(BackupPath),
+ try Fun() of
+ X ->
+ X
+ catch ErrorNS:Error:StackTrace ->
+ io:fwrite("~nError found, restoring..."),
+ {atomic, _} = mnesia:restore(BackupPath, []),
+ io:fwrite(" ok!~n"),
+
+ %% Ideally we would re-raise/re-throw the exception here with no changes.
+ %% I didn't yet find a way to do that
+ throw({ErrorNS, Error, StackTrace})
+ after
+ ok = file:delete(BackupPath)
+ end.
+
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+wait_table_loading(Tab, MonRef) ->
+ receive { load_table, Result } ->
+ %% After the `force_load` result, capture the close of the loader
+ receive {'DOWN', MonRef, _Type, _Pid, _Reason} ->
+ Result
+ end;
+
+ %% Exited without answering. This most likely indicates an error
+ {'DOWN', MonRef, _, _, Reason} ->
+ {error, Reason}
+
+ after ?FORCE_LOADING_SLEEP_TIME ->
+ case mnesia:table_info(Tab, size) of
+ {aborted, _} ->
+ ok;
+ Size ->
+ io:fwrite("[~p] Loading... (current: ~p)~n", [Tab, Size])
+ end,
+ wait_table_loading(Tab, MonRef)
+ end.
diff --git a/backend/apps/automate_storage/src/automate_storage_stats.erl b/backend/apps/automate_storage/src/automate_storage_stats.erl
index 0779d640..7514d42b 100644
--- a/backend/apps/automate_storage/src/automate_storage_stats.erl
+++ b/backend/apps/automate_storage/src/automate_storage_stats.erl
@@ -3,6 +3,8 @@
-module(automate_storage_stats).
-export([ get_user_metrics/0
+ , get_group_metrics/0
+ , get_program_metrics/0
]).
-define(SECONDS_IN_HOUR, (60 * 60)).
@@ -28,10 +30,14 @@ get_user_metrics() ->
%% User registration queries
UserMatchHead = #registered_user_entry{ id='$1'
, username='_'
+ , canonical_username='_'
, password='_'
, email='_'
, status='_'
, registration_time='$2'
+ , is_admin='_'
+ , is_advanced='_'
+ , is_in_preview='_'
},
UserResultColumn = '$1',
@@ -50,6 +56,8 @@ get_user_metrics() ->
, user_id='$1'
, session_start_time='_'
, session_last_used_time='$2'
+ , session_scope='_'
+ , session_expiration_time='_'
},
SessionResultColumn = '$1',
HourlyActiveSessionMatcher = [{ SessionMatchHead
@@ -87,6 +95,112 @@ get_user_metrics() ->
end,
mnesia:async_dirty(Transaction).
+get_group_metrics() ->
+ %% This is done in a dirty way for the sake of performance.
+ %% It's no supposed to have great consistency, but good speed.
+ CurrentTime = erlang:system_time(second),
+
+ %% Group queries
+ GroupMatchHead = #user_group_entry{ id='$1'
+ , name='_'
+ , canonical_name='_'
+ , public='_'
+ , creation_time='$2'
+ , min_level_for_private_bridge_usage='_'
+ },
+ GroupResultColumn = '$1',
+
+ CreatedGroupsLastDayMatcher = [{ GroupMatchHead
+ , [{ '>', '$2', CurrentTime - ?SECONDS_IN_DAY }]
+ , [GroupResultColumn]}],
+ CreatedGroupsLastWeekMatcher = [{ GroupMatchHead
+ , [{ '>', '$2', CurrentTime - ?SECONDS_IN_7DAY_WEEK }]
+ , [GroupResultColumn]}],
+ CreatedGroupsLastMonthMatcher = [{ GroupMatchHead
+ , [{ '>', '$2', CurrentTime - ?SECONDS_IN_28DAY_MONTH }]
+ , [GroupResultColumn]}],
+
+ Transaction = fun () ->
+ GroupCount = mnesia:table_info(?USER_GROUPS_TABLE, size),
+ CreatedGroupsLastDay = select_length(?USER_GROUPS_TABLE, CreatedGroupsLastDayMatcher),
+ CreatedGroupsLastWeek = select_length(?USER_GROUPS_TABLE, CreatedGroupsLastWeekMatcher),
+ CreatedGroupsLastMonth = select_length(?USER_GROUPS_TABLE, CreatedGroupsLastMonthMatcher),
+
+ { ok
+ , GroupCount, CreatedGroupsLastDay, CreatedGroupsLastWeek, CreatedGroupsLastMonth
+ }
+ end,
+ mnesia:async_dirty(Transaction).
+
+-spec get_program_metrics() -> {ok, #{ program_id() => #{log_severity() => non_neg_integer() }}}.
+get_program_metrics() ->
+ %% Get programs
+ Transaction = fun () ->
+ { ok
+ , cross_db_from_id_to_map(
+ ?USER_PROGRAMS_TABLE, ?USER_PROGRAM_LOGS_TABLE,
+ fun (_ProgId, Logs) ->
+ map_count_group2_by(fun(#user_program_log_entry{ severity=Severity, event_data=Data }) ->
+ Type = case Data of
+ %% Bridge errors
+ { badmatch, {error, no_connection} } ->
+ no_connection;
+
+ { program_error, {disconnected_bridge, _, _}, _ } ->
+ no_connection;
+ { program_error, {bridge_call_connection_not_found, _, _}, _ } ->
+ bridge_call_connection_not_found;
+
+ { program_error, {bridge_call_timeout, _, _}, _ } ->
+ bridge_call_timeout;
+ { program_error, {bridge_call_failed, _, _, _}, _ } ->
+ bridge_call_failed;
+ { program_error, {bridge_call_error_getting_resource, _, _}, _ } ->
+ bridge_call_error_getting_resource;
+
+ %% Program errors
+ { program_error, {variable_not_set, _}, _ } ->
+ variable_not_set;
+ { program_error, {list_not_set, _}, _ } ->
+ list_not_set;
+ { program_error, {index_not_in_list, _, _, _}, _ } ->
+ index_not_in_list;
+ { program_error, {memory_not_set, _}, _ } ->
+ memory_not_set;
+ { program_error, {memory_item_size_exceeded, _, _}, _ } ->
+ memory_item_size_exceeded;
+
+ %% Version errors
+ bad_operation ->
+ bad_operation;
+
+ %% Platform errors
+ {badmatch, {error, _}} ->
+ platform_error;
+ function_clause ->
+ platform_error;
+ {program_error, {unknown_operation}, _} ->
+ platform_error;
+
+ %% Unknown errorrs
+ undef ->
+ undefined;
+ {EventType, _} ->
+ binary:list_to_bin(
+ lists:flatten(io_lib:fwrite("unknown_~p",
+ [EventType])));
+ _ ->
+ binary:list_to_bin(
+ lists:flatten(io_lib:fwrite("unknown_~p",
+ [Data])))
+ end,
+ {Severity, Type}
+ end, Logs)
+ end)
+ }
+ end,
+ mnesia:async_dirty(Transaction).
+
%%====================================================================
%% Internal functions
%%====================================================================
@@ -102,3 +216,30 @@ select_unique_length(Tab, Matcher) ->
Unique = sets:from_list(Records),
sets:size(Unique)
end.
+
+cross_db_from_id_to_map(Left, Right, FCross) ->
+ cross_db_from_id_to_map_iter(Left, Right, FCross, mnesia:first(Left), #{}).
+
+cross_db_from_id_to_map_iter(_Left, _Right, _FCross, '$end_of_table', Acc) ->
+ Acc;
+cross_db_from_id_to_map_iter(Left, Right, FCross, Key, Acc) ->
+ Elements = mnesia:read(Right, Key),
+ NewElements = FCross(Key, Elements),
+ cross_db_from_id_to_map_iter(Left, Right, FCross, mnesia:next(Left, Key), Acc#{ Key => NewElements }).
+
+map_count_group2_by(FSelect, List) ->
+ map_count_group2_by_iter(FSelect, List, #{}).
+
+map_count_group2_by_iter(_FSelect, [], Acc) ->
+ Acc;
+map_count_group2_by_iter(FSelect, [H | T], Acc) ->
+ {Key, SubKey} = FSelect(H),
+ Values = case Acc of
+ #{ Key := Prev=#{ SubKey := Value } } ->
+ Prev#{ SubKey => Value + 1 };
+ #{ Key := Prev } ->
+ Prev#{ SubKey => 1 };
+ _ ->
+ #{ SubKey => 1 }
+ end,
+ map_count_group2_by_iter(FSelect, T, Acc#{ Key => Values }).
diff --git a/backend/apps/automate_storage/src/automate_storage_utils.erl b/backend/apps/automate_storage/src/automate_storage_utils.erl
new file mode 100644
index 00000000..8829fef5
--- /dev/null
+++ b/backend/apps/automate_storage/src/automate_storage_utils.erl
@@ -0,0 +1,67 @@
+-module(automate_storage_utils).
+
+-export([ canonicalize/1
+ , validate_username/1
+ , validate_canonicalizable/1
+ , role_has_min_level_in_group/2
+ ]).
+
+-include("./records.hrl").
+
+
+-define(VALID_CHARACTERS, "_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789").
+-define(NUMBER_CHARACTERS, "0123456789-").
+
+
+validate_canonicalizable(String) when is_binary(String) ->
+ case string:take(binary_to_list(String), ?VALID_CHARACTERS) of
+ { _, [] } ->
+ true;
+ _ ->
+ false
+ end;
+validate_canonicalizable(_String) ->
+ false.
+
+validate_not_all_numbers(String) when is_binary(String) ->
+ case string:take(binary_to_list(String), ?NUMBER_CHARACTERS) of
+ { _, [] } ->
+ false;
+ _ ->
+ true
+ end.
+
+-spec validate_username(binary()) -> boolean().
+validate_username(String)
+ when is_binary(String) and (byte_size(String) < 4) orelse (byte_size(String) > 50) ->
+ %% Length error
+ false;
+
+validate_username(String) when is_binary(String) ->
+ validate_canonicalizable(String) and validate_not_all_numbers(String);
+
+validate_username(_) ->
+ false.
+
+
+-spec canonicalize(binary()) -> binary().
+canonicalize(X) ->
+ %% Lowercasing has to be applied to the non-binary form
+ %% to properly support UTF8 characters
+ Lowercased = string:lowercase(binary_to_list(X)),
+ list_to_binary(Lowercased).
+
+
+-spec role_has_min_level_in_group(Role :: user_in_group_role(), MinLevel :: user_in_group_role() | not_allowed) -> boolean().
+role_has_min_level_in_group(_Role, not_allowed) ->
+ false;
+role_has_min_level_in_group(admin, _MinLevel) ->
+ true;
+role_has_min_level_in_group(_, admin) ->
+ false;
+role_has_min_level_in_group(editor, _) ->
+ true;
+role_has_min_level_in_group(viewer, editor) ->
+ false;
+role_has_min_level_in_group(_, viewer) ->
+ true.
diff --git a/backend/apps/automate_storage/src/automate_storage_versioning.erl b/backend/apps/automate_storage/src/automate_storage_versioning.erl
index 23500292..5afa564e 100644
--- a/backend/apps/automate_storage/src/automate_storage_versioning.erl
+++ b/backend/apps/automate_storage/src/automate_storage_versioning.erl
@@ -28,19 +28,21 @@ apply_versioning(#database_version_progression{base=Base, updates=Updates}, Node
{ok, CurrentDatabaseVersion} = get_database_version(ModuleName),
- ok = apply_updates_after_version(CurrentDatabaseVersion, Updates, ModuleName).
+ ok = apply_updates_after_version(CurrentDatabaseVersion, Updates, ModuleName),
+ ok = mnesia:sync_log().
-spec create_database(#database_version_data{}, [node()]) -> ok.
create_database(#database_version_data{ database_name=DBName
, records=Fields
, record_name=RecordName
+ , type=Type
}, Nodes) ->
case mnesia:create_table(DBName,
[ {attributes, Fields}
, { disc_copies, Nodes }
, { record_name, RecordName }
- , { type, set }
+ , { type, Type }
]) of
{ atomic, ok } ->
ok;
@@ -87,7 +89,8 @@ set_database_version(ModuleName, VersionNumber) ->
end.
-apply_updates_after_version(_, [], _) ->
+apply_updates_after_version(FinalDatabaseVersion, [], ModuleName) ->
+ io:fwrite("[~p] Version: ~p~n", [ModuleName, FinalDatabaseVersion]),
ok;
apply_updates_after_version(OldDatabaseVersion, [#database_version_transformation{ id=Id }
@@ -101,9 +104,20 @@ apply_updates_after_version(OldDatabaseVersion, [#database_version_transformatio
}
| T], ModuleName) when Id > OldDatabaseVersion ->
io:fwrite("[~p] APPLYing update ~p (current: ~p)~n", [ModuleName, Id, OldDatabaseVersion]),
- Fun(),
- set_database_version(ModuleName, Id),
- apply_updates_after_version(Id, T, ModuleName).
+ try Fun() of
+ _ ->
+ ok = set_database_version(ModuleName, Id),
+ apply_updates_after_version(Id, T, ModuleName)
+ catch ErrorNS:Error:StackTrace ->
+ io:fwrite("\033[1;37;41m Stopping due to error on update: ~p. StackTrace: ~n~p\033[0m~n", [{ErrorNS, Error}, StackTrace]),
+ try
+ Message = lists:flatten(["Update error {module=", atom_to_list(ModuleName), ", id=", integer_to_list(Id) , "}"]),
+ erlang:halt(Message, [{flush, false}])
+ of _ -> ok
+ catch _:_:_ ->
+ erlang:halt(10, [{flush, false}])
+ end
+ end.
check_updates_integrity(Updates) ->
@@ -113,6 +127,7 @@ check_updates_integrity(_, []) ->
ok;
check_updates_integrity(MinVersionLessOne, [ Update=#database_version_transformation{ id=Id
, apply=Fun
+ , revert=_
}
| T ]) when is_function(Fun) ->
if Id < MinVersionLessOne ->
diff --git a/backend/apps/automate_storage/src/databases.hrl b/backend/apps/automate_storage/src/databases.hrl
index d1bf0585..1218ce73 100644
--- a/backend/apps/automate_storage/src/databases.hrl
+++ b/backend/apps/automate_storage/src/databases.hrl
@@ -5,12 +5,25 @@
-define(REGISTERED_USERS_TABLE, automate_registered_users).
-define(USER_SESSIONS_TABLE, automate_user_sessions).
-define(USER_MONITORS_TABLE, automate_user_monitors).
+
-define(USER_PROGRAMS_TABLE, automate_user_programs).
-define(RUNNING_PROGRAMS_TABLE, automate_running_programs).
-define(PROGRAM_TAGS_TABLE, automate_program_tags).
-define(RUNNING_THREADS_TABLE, automate_running_program_threads).
+-define(USER_PROGRAM_LOGS_TABLE, automate_user_program_logs).
+-define(USER_GENERATED_LOGS_TABLE, automate_user_generated_logs).
+-define(USER_PROGRAM_EVENTS_TABLE, automate_user_program_events).
+-define(USER_PROGRAM_CHECKPOINTS_TABLE, automate_user_program_checkpoints).
+-define(PROGRAM_PAGES_TABLE, automate_user_program_pages).
+-define(PROGRAM_WIDGET_VALUE_TABLE, automate_program_widget_values).
-define(PROGRAM_VARIABLE_TABLE, automate_program_variable_table).
-define(CUSTOM_SIGNALS_TABLE, automate_custom_signals_table).
-define(USER_VERIFICATION_TABLE, automate_user_verification_table).
+-define(USER_ASSET_TABLE, automate_user_asset_table).
+-define(USER_PROFILE_LISTINGS_TABLE, automate_user_profile_listings_table).
+
+%% Groups
+-define(USER_GROUPS_TABLE, automate_user_groups).
+-define(USER_GROUP_PERMISSIONS_TABLE, automate_user_groups_permissions).
diff --git a/backend/apps/automate_storage/src/records.hrl b/backend/apps/automate_storage/src/records.hrl
index 41c4ea61..3f89c5ff 100644
--- a/backend/apps/automate_storage/src/records.hrl
+++ b/backend/apps/automate_storage/src/records.hrl
@@ -1,37 +1,195 @@
-include("../../automate_common_types/src/types.hrl").
+-ifndef(AUTOMATE_STORAGE_RECORDS).
+-define(AUTOMATE_STORAGE_RECORDS, true).
-type user_status() :: ready | mail_not_verified.
-type time_in_seconds() :: integer().
+-type time_in_milliseconds() :: integer().
--record(registered_user_entry, { id
- , username
- , password
- , email
+-record(registered_user_entry, { id :: binary() | ?MNESIA_SELECTOR
+ , username :: binary() | ?MNESIA_SELECTOR
+ , canonical_username :: binary() | ?MNESIA_SELECTOR
+ , password :: binary() | string() | ?MNESIA_SELECTOR
+ , email :: binary() | ?MNESIA_SELECTOR
, status :: user_status() | ?MNESIA_SELECTOR
, registration_time :: time_in_seconds() | ?MNESIA_SELECTOR
+
+%%% The following entries could be abstracted in a `tags` set entry, but
+%%% that would create a problem when trying to use it on mnesia:select/2 .
+ , is_admin :: boolean() | ?MNESIA_SELECTOR % Platform administration
+ , is_advanced :: boolean() | ?MNESIA_SELECTOR % Advanced features
+ , is_in_preview :: boolean() | ?MNESIA_SELECTOR % Features in beta/preview
}).
+-record(user_profile_listings_entry, { id :: owner_id()
+ , groups :: [binary()]
+ }).
+
+-type user_in_group_role() :: admin | editor | viewer.
+-type group_metadata_edition() :: #{ public => boolean(), min_level_for_private_bridge_usage => user_in_group_role() | not_allowed }.
+
+-record(user_group_entry, { id :: binary() | ?MNESIA_SELECTOR
+ , name :: binary() | ?MNESIA_SELECTOR
+ , canonical_name :: binary() | ?MNESIA_SELECTOR
+ , public :: boolean() | ?MNESIA_SELECTOR
+ , creation_time :: time_in_seconds() | ?MNESIA_SELECTOR
+ , min_level_for_private_bridge_usage :: user_in_group_role() | not_allowed | ?MNESIA_SELECTOR
+ }).
+
+-record(user_group_permissions_entry, { group_id :: binary() | ?MNESIA_SELECTOR
+ , user_id :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
+ , role :: user_in_group_role()
+ }).
+
-type verification_type() :: registration_mail_verification | password_reset_verification.
+
-record(user_verification_entry, { verification_id :: binary() | ?MNESIA_SELECTOR
, user_id :: binary() | ?MNESIA_SELECTOR
, verification_type :: verification_type() | ?MNESIA_SELECTOR
+ , creation_time :: time_in_seconds() | ?MNESIA_SELECTOR
+ , used :: boolean() | ?MNESIA_SELECTOR
}).
+-type session_expiration_time() :: session | time_in_seconds() | never.
+-type session_scope_item() :: check %% Any permission is enough for check
+ | ui %% For ui-related functions only. Not really related to permissions actually...
+ | list_custom_blocks %% Might be merged into read_program
+
+ %% Connections
+ | { edit_connection, binary() }
+ | {read_how_to_enable_service, binary()}
+ | list_connections_available | list_connections_established
+
+ %% Assets
+ | list_assets | create_assets
+
+ %% Programs
+ | list_programs | create_programs | {delete_program, binary()}
+ | {read_program, binary()} | {edit_program, binary()}
+ | {edit_program_status, binary()} | {read_program_logs, binary()} | {edit_program_metadata, binary()}
+ | {render_program, binary()}
+
+ %% Bridges
+ | list_bridges | create_bridges
+ | call_any_bridge | {call_bridge, binary(), binary()}
+ | { call_bridge_callback, binary() }
+ | {delete_bridge, binary()} | {delete_bridge_tokens, binary()}
+
+ %% Bridge signals
+ | { read_bridge_signal, binary() } %% Moderately sensitive
+ | { read_bridge_signal, binary(), binary() } %% More specific
+
+ %% Bridge shares
+ | { list_bridge_resources, binary() }
+ | { edit_connection_shares, binary() }
+
+ %% Services
+ | list_services | create_services
+
+ %% Groups
+ | list_groups | create_groups
+
+ %% Templates
+ | list_templates | create_templates
+ | {read_template, binary()} | { edit_template, binary() } | { delete_template, binary() }
+
+ %% Custom signals
+ | list_custom_signals | create_custom_signals
+
+ %% Monitors
+ | list_monitors | create_monitors
+
+ %% Private, but don't affect mechanics
+ | edit_user_profile
+ | edit_user_picture
+ | edit_user_settings
+
+ %% Actions on groups
+ | {list_group_bridges, binary()}
+ | {create_group_bridges, binary()}
+ | {list_group_programs, binary()} | { create_group_programs, binary() }
+ | {read_group_info, binary()} | {list_group_connections_available, binary()}| {list_group_connections_established, binary()}
+ | {edit_group_picture, binary()}
+ | {list_group_shares, binary()}
+
+ %% Actions on programs
+ | { list_program_shares, binary() }
+ | { call_program_bridge_callback, binary(), binary() }
+ | { list_program_connections_available, binary() } | { list_program_connections_established, binary() }
+ | { list_program_services, binary() }
+ | { read_program_variables, binary() } | { edit_program_variables, binary() }
+ | { list_program_monitors, binary() }
+
+ | { establish_program_connection, binary() } % This naming goes out
+ % of list/read/edit/delete/create/call convention.
+ %
+ % Ideally we should remove it
+
+
+
+ %% Especially sensitive
+ | { admin_group_info, binary() }
+ | { list_bridge_tokens, binary() } | { create_bridge_tokens, binary() }
+ | create_api_tokens
+
+ %% Admin-only
+ | admin_read_stats
+ | admin_list_users
+ .
+-type session_scope() :: all | [session_scope_item()].
-record(user_session_entry, { session_id
, user_id
, session_start_time :: time_in_seconds() | ?MNESIA_SELECTOR
, session_last_used_time :: time_in_seconds() | ?MNESIA_SELECTOR
+ , session_scope :: session_scope() | ?MNESIA_SELECTOR
+ , session_expiration_time :: session_expiration_time() | ?MNESIA_SELECTOR
}).
-record(user_program_entry, { id :: binary() | ?MNESIA_SELECTOR
- , user_id ::binary() | ?MNESIA_SELECTOR
- , program_name ::binary() | ?MNESIA_SELECTOR
+ , owner :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
+ , program_name :: binary() | ?MNESIA_SELECTOR
, program_type :: atom() | ?MNESIA_SELECTOR
, program_parsed :: any() | ?MNESIA_SELECTOR
, program_orig :: any() | ?MNESIA_SELECTOR
, enabled=true :: boolean() | ?MNESIA_SELECTOR
+ , program_channel :: binary() | ?MNESIA_SELECTOR
+ , creation_time :: time_in_seconds() | ?MNESIA_SELECTOR
+ , last_upload_time :: time_in_seconds() | ?MNESIA_SELECTOR
+ , last_successful_call_time :: time_in_seconds() | ?MNESIA_SELECTOR
+ , last_failed_call_time :: time_in_seconds() | ?MNESIA_SELECTOR
+ , visibility :: user_program_visibility() | ?MNESIA_SELECTOR
}).
+-type log_severity() :: debug | info | warning | error.
+-record(user_program_log_entry, { program_id :: binary() | ?MNESIA_SELECTOR
+ , thread_id :: binary() | none | ?MNESIA_SELECTOR
+ , owner :: owner_id() | none | ?OWNER_ID_MNESIA_SELECTOR
+ , block_id :: binary() | undefined | ?MNESIA_SELECTOR
+ , event_data :: _ | ?MNESIA_SELECTOR
+ , event_message :: binary() | ?MNESIA_SELECTOR
+ , event_time :: time_in_milliseconds() | ?MNESIA_SELECTOR
+ , severity :: log_severity() | ?MNESIA_SELECTOR
+ , exception_data :: none | {_, _, _} | ?MNESIA_SELECTOR
+ }).
+
+-record(user_program_editor_event, { program_id :: binary()
+ , event :: any()
+ , event_tag :: { integer(), integer() }
+ }).
+
+-record(user_program_checkpoint, { program_id :: binary()
+ , user_id :: binary()
+ , event_time :: time_in_milliseconds()
+ , content :: any()
+ }).
+
+-record(user_generated_log_entry, { program_id :: binary() | ?MNESIA_SELECTOR
+ , block_id :: binary() | undefined | ?MNESIA_SELECTOR
+ , severity :: log_severity() | ?MNESIA_SELECTOR
+ , event_time :: time_in_milliseconds() | ?MNESIA_SELECTOR
+ , event_message :: binary() | ?MNESIA_SELECTOR
+ }).
+
-record(program_tags_entry, { program_id
, tags
}).
@@ -40,7 +198,7 @@
}).
-record(monitor_entry, { id :: binary() | 'none' | ?MNESIA_SELECTOR
- , user_id :: binary() | 'none' | ?MNESIA_SELECTOR
+ , owner :: owner_id() | 'none' | ?OWNER_ID_MNESIA_SELECTOR
, type :: binary() | ?MNESIA_SELECTOR
, name :: binary() | ?MNESIA_SELECTOR
, value :: any() | ?MNESIA_SELECTOR
@@ -49,6 +207,7 @@
-record(stored_program_content, { type
, orig
, parsed
+ , pages :: map()
}).
-type program_id() :: binary().
@@ -69,6 +228,7 @@
, instruction_memory :: map() | ?MNESIA_SELECTOR
, position :: [pos_integer()] | ?MNESIA_SELECTOR
, stats :: any() | ?MNESIA_SELECTOR
+ , direction :: thread_direction() | ?MNESIA_SELECTOR
}).
-record(registered_service_entry, { registration_id :: binary() | ?MNESIA_SELECTOR
@@ -77,16 +237,37 @@
, enabled :: boolean() | ?MNESIA_SELECTOR
}).
--record(program_variable_table_entry, { id :: { binary(), binary() } % { program id, variable name }
+-record(program_variable_table_entry, { id :: { binary()
+ , binary() | {internal, _} } % { program id, variable name }
+ , program_id :: binary()
, value :: any()
}).
--record(custom_signal_entry, { id :: binary() | ?MNESIA_SELECTOR
- , name :: binary() | ?MNESIA_SELECTOR
- , owner :: binary() | ?MNESIA_SELECTOR %% User id
+-record(custom_signal_entry, { id :: binary() | ?MNESIA_SELECTOR
+ , name :: binary() | ?MNESIA_SELECTOR
+ , owner :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
}).
-record(storage_configuration_entry, { id :: any()
, value :: any()
}).
+
+-record(program_pages_entry, { page_id :: {binary(), binary()} %% {ProgramId, PagePath}
+ , program_id :: binary() %% Used for indexing on program-wide operations
+ , contents :: any() %% Type to be more strictly defined?
+ %% TODO: Access permissions?
+ }).
+
+-record(program_widget_value_entry, { widget_id :: {binary(), binary()} %% {ProgramId, <>}
+ , program_id :: binary() %% Used for indexing on program-wide operations
+ , value :: any() %% Type to be more strictly defined?
+ }).
+
+-type mime_type() :: { binary(), binary() | undefined }. %% { Type, Subtype }
+-record(user_asset_entry, { asset_id :: { owner_id(), binary() } %% { OwnerId, AssetId }
+ , owner_id :: owner_id() %% For listing
+ , mime_type :: mime_type()
+ }).
+
+-endif.
diff --git a/backend/apps/automate_storage/src/security_params.hrl b/backend/apps/automate_storage/src/security_params.hrl
new file mode 100644
index 00000000..1427bd52
--- /dev/null
+++ b/backend/apps/automate_storage/src/security_params.hrl
@@ -0,0 +1,18 @@
+%% Password hashing parameters
+-ifndef (TEST).
+-define(PASSWORD_HASHING_PARALLELISM, 1). % Not more than one thread will be used for this at once
+-define(PASSWORD_HASHING_OPS_LIMIT, 8). % Default value from argon2_elixir
+-define(PASSWORD_HASHING_MEM_LIMIT, 32768). % 32MiB. Was libsodium:memlimit_interactive()
+-define(PASSWORD_HASHING_HASHLEN, 32). % Default value from argon2_elixir
+-define(PASSWORD_HASHING_SALTLEN, 16). % Default value from argon2_elixir
+-else.
+%% Parameters for testing environment. Make the login and registering significatively lighter.
+-define(PASSWORD_HASHING_PARALLELISM, 1). % Not more than one thread will be used for this at once
+-define(PASSWORD_HASHING_OPS_LIMIT, 1). % Default value from argon2_elixir
+-define(PASSWORD_HASHING_MEM_LIMIT, 1024). % 32MiB. Was libsodium:memlimit_interactive()
+-define(PASSWORD_HASHING_HASHLEN, 32). % Default value from argon2_elixir
+-define(PASSWORD_HASHING_SALTLEN, 16). % Default value from argon2_elixir
+-endif.
+
+%% Token generation parameters
+-define(KEY_RANDOM_LENGTH, 30).
diff --git a/backend/apps/automate_storage/src/versioning.hrl b/backend/apps/automate_storage/src/versioning.hrl
index 61026abd..c6b0bc92 100644
--- a/backend/apps/automate_storage/src/versioning.hrl
+++ b/backend/apps/automate_storage/src/versioning.hrl
@@ -7,6 +7,7 @@
-type database_version_transformation_id() :: pos_integer().
-record(database_version_transformation, { id :: database_version_transformation_id()
, apply :: function()
+ , revert=undefined :: function() | undefined %% Used for debugging migrations
}).
-record(database_version_progression, { base :: [#database_version_data{}]
diff --git a/backend/apps/automate_storage/test/automate_storage_logs_tests.erl b/backend/apps/automate_storage/test/automate_storage_logs_tests.erl
new file mode 100644
index 00000000..adda22cb
--- /dev/null
+++ b/backend/apps/automate_storage/test/automate_storage_logs_tests.erl
@@ -0,0 +1,125 @@
+%%% @doc
+%%% Automate channel engine tests.
+%%% @end
+
+-module(automate_storage_logs_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(APPLICATION, automate_storage).
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+
+-define(TEST_NODES, [node()]).
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+session_manager_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ NodeName = node(),
+
+ %% Use a custom node name to avoid overwriting the actual databases
+ net_kernel:start([testing, shortnames]),
+
+ {ok, Pid} = application:ensure_all_started(automate_storage),
+
+ {NodeName, Pid}.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop({_NodeName, _Pid}) ->
+ application:stop(automate_storage),
+
+ ok.
+
+%%====================================================================
+%% Tests
+%%====================================================================
+tests(_SetupResult) ->
+ [ {"[Storage program logs] Check program error log watermarks", fun test_error_logs_watermarks/0}
+ , {"[Storage program logs] Check user log watermarks", fun test_user_logs_watermarks/0}
+ ].
+
+test_error_logs_watermarks() ->
+ {LowWatermark, HighWatermark} = automate_configuration:get_program_logs_watermarks(),
+ ProgramId = <<"automate_storage_logs_tests/test_watermarks">>,
+
+ %% Generate and insert first batch
+ Entries = lists:map(fun(Idx) -> gen_log_with_id(Idx, ProgramId) end, lists:seq(1, HighWatermark)),
+ ok = lists:foreach(fun automate_storage:log_program_error/1, Entries),
+
+ %% Check that it reaches up until HighWatermark
+ {ok, FilledLogs} = automate_storage:get_logs_from_program_id(ProgramId),
+ ?assertMatch(HighWatermark, length(FilledLogs)),
+
+ %% Add the one log that overflows the watermark
+ automate_storage:log_program_error(gen_log_with_id(HighWatermark + 1, ProgramId)),
+
+ %% Check that now the LowWatermark is in use
+ {ok, EmptiedLogs} = automate_storage:get_logs_from_program_id(ProgramId),
+ ?assertMatch(LowWatermark, length(EmptiedLogs)),
+
+ %% All logs present are the more recent ones
+ Deleted = HighWatermark - LowWatermark,
+ Latest = HighWatermark + 1,
+ lists:foreach(fun(#user_program_log_entry{ event_time=Idx }) ->
+ ?assert(Idx > (Latest - Deleted))
+ end, EmptiedLogs).
+
+test_user_logs_watermarks() ->
+ {LowWatermark, HighWatermark} = automate_configuration:get_program_logs_watermarks(),
+ ProgramId = <<"automate_storage_logs_tests/test_watermarks">>,
+
+ %% Generate and insert first batch
+ Entries = lists:map(fun(Idx) -> gen_user_log_with_id(Idx, ProgramId) end, lists:seq(1, HighWatermark)),
+ ok = lists:foreach(fun automate_storage:add_user_generated_log/1, Entries),
+
+ %% Check that it reaches up until HighWatermark
+ {ok, FilledLogs} = automate_storage:get_user_generated_logs(ProgramId),
+ ?assertMatch(HighWatermark, length(FilledLogs)),
+
+ %% Add the one log that overflows the watermark
+ automate_storage:add_user_generated_log(gen_user_log_with_id(HighWatermark + 1, ProgramId)),
+
+ %% Check that now the LowWatermark is in use
+ {ok, EmptiedLogs} = automate_storage:get_user_generated_logs(ProgramId),
+ ?assertMatch(LowWatermark, length(EmptiedLogs)),
+
+ %% All logs present are the more recent ones
+ Deleted = HighWatermark - LowWatermark,
+ Latest = HighWatermark + 1,
+ lists:foreach(fun(#user_generated_log_entry{ event_time=Idx }) ->
+ ?assert(Idx > (Latest - Deleted))
+ end, EmptiedLogs).
+
+%%====================================================================
+%% Internal
+%%====================================================================
+gen_log_with_id(Idx, ProgramId) ->
+ #user_program_log_entry{ program_id=ProgramId
+ , thread_id=none
+ , owner=none
+ , block_id=undefined
+ , event_data=Idx
+ , event_message= <<"test">>
+ , event_time=Idx
+ , severity=error
+ , exception_data=none
+ }.
+
+gen_user_log_with_id(Idx, ProgramId) ->
+ #user_generated_log_entry{ program_id=ProgramId
+ , block_id=undefined
+ , severity=error
+ , event_message= <<"test">>
+ , event_time=Idx
+ }.
diff --git a/backend/apps/automate_storage/test/automate_storage_username_test.erl b/backend/apps/automate_storage/test/automate_storage_username_test.erl
new file mode 100644
index 00000000..0aa4663f
--- /dev/null
+++ b/backend/apps/automate_storage/test/automate_storage_username_test.erl
@@ -0,0 +1,91 @@
+%%% @doc
+%%% Automate storage username management tests.
+%%% @end
+
+-module(automate_storage_username_test).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(LIB, automate_storage_utils).
+%% Data structures
+-include("../../automate_storage/src/records.hrl").
+
+%%====================================================================
+%% Test API
+%%====================================================================
+
+username_management_test_() ->
+ {setup
+ , fun setup/0
+ , fun stop/1
+ , fun tests/1
+ }.
+
+%% @doc App infrastructure setup.
+%% @end
+setup() ->
+ ok.
+
+%% @doc App infrastructure teardown.
+%% @end
+stop(_) ->
+ ok.
+
+tests(_SetupResult) ->
+ [ {"[Canonicalization] UTF8 lowercasing", fun utf8_lowercasing/0}
+ , {"[ValidateUsername] Valid usernames", fun valid_usernames/0}
+ , {"[ValidateUsername] InValid usernames", fun invalid_usernames/0}
+ ].
+
+utf8_lowercasing() ->
+ ?assertEqual(<<"123ñ456ñ">>, ?LIB:canonicalize(<<"123Ñ456ñ">>)).
+
+valid_usernames() ->
+ ?assertEqual(true, ?LIB:validate_username(<<"test">>)),
+ ?assertEqual(true, ?LIB:validate_username(<<"test_">>)),
+ ?assertEqual(true, ?LIB:validate_username(<<"_test">>)),
+ ?assertEqual(true, ?LIB:validate_username(<<"_test123">>)),
+ ?assertEqual(true, ?LIB:validate_username(<<"test123">>)),
+ ?assertEqual(true, ?LIB:validate_username(<<"UPPERCASE">>)),
+ ok.
+
+invalid_usernames() ->
+ %% 0 characters
+ ?assertEqual(false, ?LIB:validate_username(<<"">>)),
+ %% 1 characters
+ ?assertEqual(false, ?LIB:validate_username(<<"t">>)),
+ %% 2 characters
+ ?assertEqual(false, ?LIB:validate_username(<<"te">>)),
+ %% 51 characters
+ ?assertEqual(false, ?LIB:validate_username(<<"a""1234567890""1234567890""1234567890""1234567890""1234567890">>)),
+ %% Only numbers with or without dashes
+ ?assertEqual(false, ?LIB:validate_username(<<"123456">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"123-456">>)),
+ %% Bad types
+ ?assertEqual(false, ?LIB:validate_username(undefined)),
+ ?assertEqual(false, ?LIB:validate_username(null)),
+ %% Invalid characters
+ ?assertEqual(false, ?LIB:validate_username(<<"123 456">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"123\n456">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"123?456">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"">>)),
+ %% Unicode non-printable marks
+ ?assertEqual(false, ?LIB:validate_username(<<"test\xc2\xa0test">>)), % Non printable space
+
+ %% Language names, from https://tour.golang.org/welcome/2
+ ?assertEqual(false, ?LIB:validate_username(<<"Português">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Català">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Chinese_中文">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Simplified_chinese_简体">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Traditional_chinese_繁體">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Česky">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Français">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Hebrew_עִבְרִית">>)), % Note Right-To-Left encoding
+ ?assertEqual(false, ?LIB:validate_username(<<"Japanese_日本語">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Korean_한국어">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Română">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Русский">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Thai_ภาษาไทย">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Türkçe">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Ukrainian_Українська">>)),
+ ?assertEqual(false, ?LIB:validate_username(<<"Uzbek_Ўзбекча">>)),
+ ok.
diff --git a/backend/apps/automate_template_engine/src/automate_template_engine.erl b/backend/apps/automate_template_engine/src/automate_template_engine.erl
index ed738465..2cb72e6a 100644
--- a/backend/apps/automate_template_engine/src/automate_template_engine.erl
+++ b/backend/apps/automate_template_engine/src/automate_template_engine.erl
@@ -6,7 +6,7 @@
-module(automate_template_engine).
%% API
--export([ list_templates_from_user_id/1
+-export([ list_templates/1
, create_template/3
, delete_template/2
, update_template/4
@@ -18,32 +18,34 @@
-define(MATCHING, automate_template_engine_matching).
-define(BACKEND, automate_template_engine_mnesia_backend).
-include("records.hrl").
+-include("../../automate_bot_engine/src/program_records.hrl").
%%====================================================================
%% API functions
%%====================================================================
--spec list_templates_from_user_id(binary()) -> {ok, [#template_entry{}]}.
-list_templates_from_user_id(UserId) ->
- ?BACKEND:list_templates_from_user_id(UserId).
+-spec list_templates(owner_id()) -> {ok, [#template_entry{}]}.
+list_templates(Owner) ->
+ ?BACKEND:list_templates(Owner).
--spec create_template(binary(), binary(), [any()]) -> {ok, binary()}.
-create_template(UserId, TemplateName, TemplateContent) ->
- ?BACKEND:create_template(UserId, TemplateName, TemplateContent).
+-spec create_template(owner_id(), binary(), [any()]) -> {ok, binary()}.
+create_template(Owner, TemplateName, TemplateContent) ->
+ ?BACKEND:create_template(Owner, TemplateName, TemplateContent).
--spec delete_template(binary(), binary()) -> ok | {error, binary()}.
-delete_template(UserId, TemplateId) ->
- ?BACKEND:delete_template(UserId, TemplateId).
+-spec delete_template(owner_id(), binary()) -> ok | {error, binary()}.
+delete_template(Owner, TemplateId) ->
+ ?BACKEND:delete_template(Owner, TemplateId).
--spec update_template(binary(), binary(), binary(), [any()]) -> ok | {error, binary()}.
-update_template(UserId, TemplateId, TemplateName, TemplateContent) ->
- ?BACKEND:update_template(UserId, TemplateId, TemplateName, TemplateContent).
+-spec update_template(owner_id(), binary(), binary(), [any()]) -> ok | {error, binary()}.
+update_template(Owner, TemplateId, TemplateName, TemplateContent) ->
+ ?BACKEND:update_template(Owner, TemplateId, TemplateName, TemplateContent).
--spec get_template(binary(), binary()) -> {ok, #template_entry{}} | {error, binary()}.
-get_template(UserId, TemplateId) ->
- ?BACKEND:get_template(UserId, TemplateId).
+-spec get_template(owner_id(), binary()) -> {ok, #template_entry{}} | {error, binary()}.
+get_template(Owner, TemplateId) ->
+ ?BACKEND:get_template(Owner, TemplateId).
-match(UserId, Thread, TemplateId, InputValue) ->
- ?MATCHING:match(UserId, Thread, TemplateId, InputValue).
+-spec match(owner_id(), #program_thread{}, binary(), binary()) -> {ok, #program_thread{}, any()} | {error, not_found}.
+match(Owner, Thread, TemplateId, InputValue) ->
+ ?MATCHING:match(Owner, Thread, TemplateId, InputValue).
diff --git a/backend/apps/automate_template_engine/src/automate_template_engine_app.erl b/backend/apps/automate_template_engine/src/automate_template_engine_app.erl
index cf71952c..c6f8d9bc 100644
--- a/backend/apps/automate_template_engine/src/automate_template_engine_app.erl
+++ b/backend/apps/automate_template_engine/src/automate_template_engine_app.erl
@@ -8,14 +8,16 @@
-behaviour(application).
%% Application callbacks
--export([start/2, stop/1]).
+-export([start/0, start/2, stop/1]).
%%====================================================================
%% API
%%====================================================================
+start() ->
+ automate_template_engine_sup:start_link().
start(_StartType, _StartArgs) ->
- automate_template_engine_sup:start_link().
+ start().
%%--------------------------------------------------------------------
stop(_State) ->
diff --git a/backend/apps/automate_template_engine/src/automate_template_engine_configuration.erl b/backend/apps/automate_template_engine/src/automate_template_engine_configuration.erl
index 516af988..4b9402aa 100644
--- a/backend/apps/automate_template_engine/src/automate_template_engine_configuration.erl
+++ b/backend/apps/automate_template_engine/src/automate_template_engine_configuration.erl
@@ -21,5 +21,29 @@ get_versioning(_Nodes) ->
#database_version_progression
{ base=Version_1
- , updates=[]
+ , updates=
+ %% Introduce user groups
+ [ #database_version_transformation
+ { id=1
+ , apply=fun() ->
+ {atomic, ok} = mnesia:transform_table(
+ ?TEMPLATE_TABLE,
+ fun({ template_entry
+ , Id, Name, Owner, Content
+ }) ->
+ { template_entry
+ , Id, Name, {user, Owner}, Content
+ }
+ end,
+ [ id, name, owner, content
+ ],
+ template_entry
+ ),
+
+ ok = mnesia:wait_for_tables([ ?TEMPLATE_TABLE ],
+ automate_configuration:get_table_wait_time())
+
+ end
+ }
+ ]
}.
diff --git a/backend/apps/automate_template_engine/src/automate_template_engine_matching.erl b/backend/apps/automate_template_engine/src/automate_template_engine_matching.erl
index 1afeb71c..7a6f0328 100644
--- a/backend/apps/automate_template_engine/src/automate_template_engine_matching.erl
+++ b/backend/apps/automate_template_engine/src/automate_template_engine_matching.erl
@@ -11,12 +11,13 @@
-define(BACKEND, automate_template_engine_mnesia_backend).
-include("records.hrl").
+-include("../../automate_bot_engine/src/program_records.hrl").
%%====================================================================
%% API functions
%%====================================================================
-match(UserId, Thread, TemplateId, InputValue) ->
- case ?BACKEND:get_template(UserId, TemplateId) of
+match(Owner, Thread, TemplateId, InputValue) ->
+ case ?BACKEND:get_template(Owner, TemplateId) of
{ok, #template_entry{ content=Content
}} ->
match_content(Thread, Content, InputValue)
@@ -42,9 +43,9 @@ match_content(Thread, Template, InputValue) ->
set_variables(_Original, [], [], Thread) ->
{ok, Thread};
-set_variables(Original, [{Start, Len} | Matches], [Variable | Variables], Thread) ->
+set_variables(Original, [{Start, Len} | Matches], [Variable | Variables], Thread=#program_thread{ program_id=ProgramId }) ->
Value = binary:part(Original, Start, Len),
- {ok, NewThread} = automate_bot_engine_variables:set_program_variable(Thread, Variable, Value),
+ ok = automate_bot_engine_variables:set_program_variable(ProgramId, Variable, Value, undefined),
set_variables(Original, Matches, Variables, Thread).
diff --git a/backend/apps/automate_template_engine/src/automate_template_engine_mnesia_backend.erl b/backend/apps/automate_template_engine/src/automate_template_engine_mnesia_backend.erl
index 5da30f97..ecf783ac 100644
--- a/backend/apps/automate_template_engine/src/automate_template_engine_mnesia_backend.erl
+++ b/backend/apps/automate_template_engine/src/automate_template_engine_mnesia_backend.erl
@@ -9,7 +9,7 @@
]).
%% API
--export([ list_templates_from_user_id/1
+-export([ list_templates/1
, create_template/3
, delete_template/2
, update_template/4
@@ -30,18 +30,20 @@ start_link() ->
ignore.
--spec list_templates_from_user_id(binary()) -> {ok, [map()]}.
-list_templates_from_user_id(UserId) ->
+-spec list_templates(owner_id()) -> {ok, [map()]}.
+list_templates({OwnerType, OwnerId}) ->
Transaction = fun() ->
%% Find userid with that name
MatchHead = #template_entry{ id='_'
, name='_'
- , owner='$1'
+ , owner={'$1', '$2'}
, content='_'
},
- Guard = {'==', '$1', UserId},
+ Guards = [ {'==', '$1', OwnerType}
+ , {'==', '$2', OwnerId}
+ ],
ResultColumn = '$_',
- Matcher = [{MatchHead, [Guard], [ResultColumn]}],
+ Matcher = [{MatchHead, Guards, [ResultColumn]}],
{ok, mnesia:select(?TEMPLATE_TABLE, Matcher)}
end,
@@ -53,12 +55,12 @@ list_templates_from_user_id(UserId) ->
end.
--spec create_template(binary(), binary(), [any()]) -> {ok, binary()}.
-create_template(UserId, TemplateName, TemplateContent) ->
+-spec create_template(owner_id(), binary(), [any()]) -> {ok, binary()}.
+create_template(Owner, TemplateName, TemplateContent) ->
Id = generate_id(),
Entry = #template_entry{ id=Id
, name=TemplateName
- , owner=UserId
+ , owner=Owner
, content=TemplateContent
},
@@ -73,14 +75,14 @@ create_template(UserId, TemplateName, TemplateContent) ->
{error, mnesia:error_description(Reason)}
end.
--spec delete_template(binary(), binary()) -> ok | {error, binary()}.
-delete_template(UserId, TemplateId) ->
+-spec delete_template(owner_id(), binary()) -> ok | {error, binary()}.
+delete_template(Owner, TemplateId) ->
Transaction = fun() ->
case mnesia:read(?TEMPLATE_TABLE, TemplateId) of
[#template_entry{ owner=OwnerId
}] ->
case OwnerId of
- UserId ->
+ Owner ->
ok = mnesia:delete(?TEMPLATE_TABLE, TemplateId, write),
ok;
_ ->
@@ -99,11 +101,11 @@ delete_template(UserId, TemplateId) ->
--spec update_template(binary(), binary(), binary(), [any()]) -> ok | {error, binary()}.
-update_template(UserId, TemplateId, TemplateName, TemplateContent) ->
+-spec update_template(owner_id(), binary(), binary(), [any()]) -> ok | {error, binary()}.
+update_template(Owner, TemplateId, TemplateName, TemplateContent) ->
Entry = #template_entry{ id=TemplateId
, name=TemplateName
- , owner=UserId
+ , owner=Owner
, content=TemplateContent
},
Transaction = fun() ->
@@ -111,7 +113,7 @@ update_template(UserId, TemplateId, TemplateName, TemplateContent) ->
[#template_entry{ owner=OwnerId
}] ->
case OwnerId of
- UserId ->
+ Owner ->
ok = mnesia:write(?TEMPLATE_TABLE, Entry, write),
ok;
_ ->
@@ -130,15 +132,15 @@ update_template(UserId, TemplateId, TemplateName, TemplateContent) ->
--spec get_template(binary(), binary()) -> {ok, #template_entry{}} | {error, binary()}.
-get_template(UserId, TemplateId) ->
+-spec get_template(owner_id(), binary()) -> {ok, #template_entry{}} | {error, binary()}.
+get_template(Owner, TemplateId) ->
Transaction = fun() ->
case mnesia:read(?TEMPLATE_TABLE, TemplateId) of
[Entry=#template_entry{ owner=OwnerId
}] ->
case OwnerId of
- UserId ->
+ Owner ->
{ok, Entry};
_ ->
{error, unauthorized}
@@ -160,5 +162,3 @@ get_template(UserId, TemplateId) ->
%%====================================================================
generate_id() ->
binary:list_to_bin(uuid:to_string(uuid:uuid4())).
-
-
diff --git a/backend/apps/automate_template_engine/src/records.hrl b/backend/apps/automate_template_engine/src/records.hrl
index 45608679..8488fc87 100644
--- a/backend/apps/automate_template_engine/src/records.hrl
+++ b/backend/apps/automate_template_engine/src/records.hrl
@@ -1,7 +1,7 @@
-include("../../automate_common_types/src/types.hrl").
--record(template_entry, { id :: binary() | ?MNESIA_SELECTOR
- , name :: binary() | ?MNESIA_SELECTOR
- , owner :: binary() | ?MNESIA_SELECTOR %% User id
- , content :: [any()] | ?MNESIA_SELECTOR
+-record(template_entry, { id :: binary() | ?MNESIA_SELECTOR
+ , name :: binary() | ?MNESIA_SELECTOR
+ , owner :: owner_id() | ?OWNER_ID_MNESIA_SELECTOR
+ , content :: [any()] | ?MNESIA_SELECTOR
}).
diff --git a/backend/apps/automate_testing/src/automate_testing.app.src b/backend/apps/automate_testing/src/automate_testing.app.src
new file mode 100644
index 00000000..ed7fd684
--- /dev/null
+++ b/backend/apps/automate_testing/src/automate_testing.app.src
@@ -0,0 +1,14 @@
+{application, automate_testing,
+ [
+ {description, "Auto-mate testing tools."},
+ {vsn, "0.0.0"},
+ {registered, []},
+ {applications, [ stdlib
+ , kernel
+ ]},
+ {env, [
+ ]},
+ {modules, []},
+ {licenses, ["Apache 2.0"]},
+ {links, []}
+ ]}.
diff --git a/backend/apps/automate_testing/src/automate_testing.erl b/backend/apps/automate_testing/src/automate_testing.erl
new file mode 100644
index 00000000..4d9f4300
--- /dev/null
+++ b/backend/apps/automate_testing/src/automate_testing.erl
@@ -0,0 +1,40 @@
+-module(automate_testing).
+
+-export([ apply_time/1
+ , set_corrected_time/1
+ , unset_corrected_time/0
+ ]).
+
+apply_time(X) ->
+ case ets:whereis(time_tests) of
+ undefined ->
+ X;
+ _ ->
+ case ets:lookup(time_tests, correction) of
+ [] ->
+ X;
+ [{correction, C}] ->
+ { MegaS, S, MicroS } = X,
+ Corrected = { MegaS, S + C, MicroS },
+ Corrected
+ end
+ end.
+
+set_corrected_time(CorrectedTimestamp) ->
+ case ets:whereis(time_tests) of
+ undefined ->
+ ets:new(time_tests, [ordered_set, public, named_table]),
+ ok;
+ _ ->
+ ok
+ end,
+
+ Now = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(erlang:timestamp())),
+ Corrected = calendar:datetime_to_gregorian_seconds(CorrectedTimestamp),
+ Diff = Corrected - Now,
+
+ true = ets:insert(time_tests, { correction, Diff }),
+ ok.
+
+unset_corrected_time() ->
+ erase(time_correction).
diff --git a/backend/apps/automate_testing/src/testing.hrl b/backend/apps/automate_testing/src/testing.hrl
new file mode 100644
index 00000000..c7ba0fd4
--- /dev/null
+++ b/backend/apps/automate_testing/src/testing.hrl
@@ -0,0 +1,5 @@
+-ifdef(TEST).
+-define(CORRECT_EXECUTION_TIME(X), automate_testing:apply_time(X)).
+-else.
+-define(CORRECT_EXECUTION_TIME(X), X).
+-endif.
diff --git a/backend/config/sys.config.orig b/backend/config/sys.config.orig
index a9bd4d15..3046975f 100644
--- a/backend/config/sys.config.orig
+++ b/backend/config/sys.config.orig
@@ -1,11 +1,34 @@
-[ { automate, [ { table_wait_time, 10000 } ] }
+[ { automate, [ { table_wait_time, 10000 }
+ %% , { asset_directory, "/path/to/asset/directory" }
+
+ %% Uncomment the following configuration for development environments
+ %% where the frontend is on port 4200 and the REST API in port 8888 .
+ %%
+ %% , { frontend_root_url, <<"http://localhost:4200">> }
+
+ %% Uncomment the following for environments where the API is behind a proxy that
+ %% routes the queries and change Schema/Host/PortNum.
+ %% Remember to update the values with the appropriate ones!
+ %%
+ %% , { backend_api_info, #{ scheme => <<"https">>
+ %% , host => <<"programaker.com">>
+ %% , port => 443
+ %% } }
+
+ %% Uncomment the following to set the watermarks of the user program logs.
+ %% When the HIGH watermark is reached for a single log, the logs will be
+ %% drained, starting with the older one, until the LOW watermark is reached.
+ %% , { user_program_logs_count_low_watermark, 1000 }
+ %% , { user_program_logs_count_high_watermark, 2000 }
+
+ ] }
, { automate_rest_api, [ { port, 8888}
%% Commenting the following line removes the authentication on /metrics
, { metrics_secret, <<"@INSERT HERE RANDOM SECRET FOR METRICS@">> }
]}
%% To support mail, replace 'none' with the endpoint of your mail gateway API
- %% Eg: "http://plaza-mail-gateway:80/mail/send"
+ %% Eg: "http://programaker-mail-gateway:80/mail/send"
, { automate_mail, [ { mail_gateway, none }
%% %% Below, options that can be used if a 'mail_gateway' is configured.
%% , { registration_verification_url_pattern, "https://programaker.com/register/verify/~s" }
@@ -14,15 +37,27 @@
%% , { password_reset_verification_sender, <<"PrograMaker Team">> }
%% , { platform_name, <<"PrograMaker">> }
] }
-%% If you uncomment this you can add elastic search logs and exclude some channels.
-%%, { automate_logging, [ { endpoint, [ #{ "type" => elasticsearch
-%% , "url" => "@INSERT HERE YOUR ES SERVER URL"
-%% , "index_prefix" => "plaza"
-%% , "exclude_bridges" => [ <<"0093325b-373f-4f1c-bace-4532cce79df4">> %% Time
-%% ]
-%% , "user" => "USER"
-%% , "password" => "PASSWORD"
-%% }
-%% ] }
-%% ]}
+
+%% %% Uncomment to allow the users to save the bridge signals
+%% , { automate_logging, [ { signal_storage_endpoint, #{ type => raw
+%% , url => <<"http://localhost:5000">> %% Your server URL
+%% }
+%% }
+%% %% Uncomment to allow the users to track program calls
+%% , { program_call_log_storage_endpoint, #{ type => raw
+%% , url => <<"http://localhost:5000">> %% Your server URL
+%% }
+%% %% ↓ Deprecated, is preferrable to allow the users to configure which bridges are to be recorded
+%% %% Uncomment this to send everything (except excluded bridges) to an ElasticSearch server.
+%% , { endpoint, [ #{ "type" => elasticsearch
+%% , "url" => "@INSERT HERE YOUR ES SERVER URL"
+%% , "index_prefix" => "programaker"
+%% , "exclude_bridges" => [ <<"0093325b-373f-4f1c-bace-4532cce79df4">> %% Time
+%% ]
+%% , "user" => "USER"
+%% , "password" => "PASSWORD"
+%% }
+%% ]
+%% }
+%% ]}
].
diff --git a/backend/config/vm.args b/backend/config/vm.args
index d4f024e5..5266e909 100644
--- a/backend/config/vm.args
+++ b/backend/config/vm.args
@@ -1,4 +1,4 @@
--sname plaza
+-sname programaker
+K true
+A30
diff --git a/backend/get-deps.sh b/backend/get-deps.sh
new file mode 100755
index 00000000..255e9cdb
--- /dev/null
+++ b/backend/get-deps.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -eu
+
+# Start by pulling normal dependencies
+rebar3 get-deps
+
+# Then retrieve eargon2's submodules
+cd _build/default/lib/eargon2/
+git submodule update --init --recursive
diff --git a/backend/rebar.config b/backend/rebar.config
index 363cd1c3..f0cc7a17 100644
--- a/backend/rebar.config
+++ b/backend/rebar.config
@@ -1,55 +1,58 @@
{erl_opts, [debug_info]}.
{deps, [ { cowboy, ".*"
, { git,"https://github.com/ninenines/cowboy"
- , {tag, "2.6.3"}}}
+ , {tag, "2.9.0"}}}
, {jiffy, ".*"
, { git, "https://github.com/davisp/jiffy"
- , {tag, "0.15.2"}}}
+ , {tag, "1.0.8"}}}
, {uuid, ".*"
, {git, "https://github.com/avtobiff/erlang-uuid"
, {tag, "v0.5.2"}}}
- , {libsodium, ".*"
- , {git, "https://github.com/potatosalad/erlang-libsodium"
- , {tag, "0.0.10"}}}
+ , {eargon2, ".*"
+ , {git, "https://github.com/ergenius/eargon2"
+ , {ref, "949dfc7c3e1bd06843b3eca823c92e9ed0c66199"}}}
, {mochiweb, ".*"
, {git, "https://github.com/mochi/mochiweb"
- , {tag, "v2.19.0"}}}
+ , {tag, "v2.21.0"}}}
, {mochiweb_xpath, ".*"
, {git, "https://github.com/retnuh/mochiweb_xpath"
, {tag, "v1.2.0"}}}
-
, {prometheus, ".*"
- , {git, "https://github.com/deadtrickster/prometheus.erl/"
- , {tag, "v4.2.0" }}}
+ , {git, "https://github.com/deadtrickster/prometheus.erl"
+ , {tag, "v4.8.1" }}}
+ , {qdate, ".*"
+ , {git, "http://github.com/choptastic/qdate"
+ , {tag, "0.5.0" }}}
+ , {recon, ".*"
+ , {git, "https://github.com/ferd/recon"
+ , {tag, "2.5.1" }}}
]}.
{dialyzer, [ {get_warnings, true}
, {exclude_apps, [ cowboy
, mochiweb
, mochiweb_xpath
+ , eargon2
+ , qdate
]}
]}.
{relx, [{release, { automate, "0.0.1" },
[ automate
, sasl
- , automate_rest_api
- , automate_bot_engine
- , automate_monitor_engine
- , automate_channel_engine
- , automate_service_registry
- , automate_storage
- , automate_coordination
- , automate_stats
- , automate_logging
- , automate_mail
-
- , automate_services_time
- , automate_program_linker
- , automate_service_port_engine
- , automate_services_all
+ %% Profiling components
+ , tools
+ , observer
+ , runtime_tools
+ , recon
+ , os_mon
- , automate_configuration
+ %% Dependencies
+ , cowboy
+ , prometheus
+ , mochiweb
+ , eargon2
+ , qdate
]},
{sys_config, "./config/sys.config"},
diff --git a/backend/rebar.lock b/backend/rebar.lock
index e73ee9e6..71eaf4a5 100644
--- a/backend/rebar.lock
+++ b/backend/rebar.lock
@@ -1,36 +1,61 @@
-[{<<"cowboy">>,
+{"1.2.0",
+[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},2},
+ {<<"cowboy">>,
{git,"https://github.com/ninenines/cowboy",
- {ref,"28d3515d716d32d1700fa21e904c613c139d4019"}},
+ {ref,"04ca4c5d31a92d4d3de087bbd7d6021dc4a6d409"}},
0},
{<<"cowlib">>,
{git,"https://github.com/ninenines/cowlib",
- {ref,"56a3fee151340212c1b7a92344e4ece07fc15010"}},
+ {ref,"e9448e5628c8c1d9083223ff973af8de31a566d1"}},
1},
- {<<"jiffy">>,
- {git,"https://github.com/davisp/jiffy/",
- {ref,"c942525130ff0271bd318715406f234c0dc9bc5a"}},
+ {<<"eargon2">>,
+ {git,"https://github.com/ergenius/eargon2",
+ {ref,"949dfc7c3e1bd06843b3eca823c92e9ed0c66199"}},
0},
- {<<"libsodium">>,
- {git,"git://github.com/potatosalad/erlang-libsodium.git",
- {ref,"aacf353461c27fc8732f69ecfcf1aaed9d81d366"}},
+ {<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"1.5.0">>},1},
+ {<<"jiffy">>,
+ {git,"https://github.com/davisp/jiffy",
+ {ref,"effc3c9a68478b692523e61b308ad9257c1ddeca"}},
0},
{<<"mochiweb">>,
- {git,"git://github.com/mochi/mochiweb.git",
- {ref,"d3d3d7bd54d87afa45470a540d41d62e19a31400"}},
+ {git,"https://github.com/mochi/mochiweb",
+ {ref,"db5408965ef53f31d073db64fbd3332198743cfe"}},
0},
{<<"mochiweb_xpath">>,
{git,"https://github.com/retnuh/mochiweb_xpath",
{ref,"ccea7319e88281f6274b6fce617278c434bac56f"}},
0},
{<<"prometheus">>,
- {git,"https://github.com/deadtrickster/prometheus.erl/",
- {ref,"7e98c67ca95fa4f1fd1bdc81f75df2cabdbc36fa"}},
+ {git,"https://github.com/deadtrickster/prometheus.erl",
+ {ref,"3245220e5b51c8005c84c2683fda1108b736badd"}},
0},
+ {<<"qdate">>,
+ {git,"http://github.com/choptastic/qdate",
+ {ref,"b8a50750267af6480333a1e4ee27e9db91b2c422"}},
+ 0},
+ {<<"qdate_localtime">>,{pkg,<<"qdate_localtime">>,<<"1.1.0">>},1},
+ {<<"quantile_estimator">>,{pkg,<<"quantile_estimator">>,<<"0.2.1">>},1},
{<<"ranch">>,
{git,"https://github.com/ninenines/ranch",
- {ref,"8eaae552ec520605b7f4b0e5943256d2b5e3621c"}},
+ {ref,"a692f44567034dacf5efcaa24a24183788594eb7"}},
1},
+ {<<"recon">>,
+ {git,"https://github.com/ferd/recon",
+ {ref,"f7b6c08e6e9e2219db58bfb012c58c178822e01e"}},
+ 0},
{<<"uuid">>,
- {git,"https://github.com/avtobiff/erlang-uuid.git",
- {ref,"1fdc1b367902da71b774a34ae15690811ac17b99"}},
- 0}].
+ {git,"https://github.com/avtobiff/erlang-uuid",
+ {ref,"cb02a2039a9b29dd2eef0446039c9c6e164df9ef"}},
+ 0}]}.
+[
+{pkg_hash,[
+ {<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>},
+ {<<"erlware_commons">>, <<"918C56D8FB3BE52AF0DF138ED6E0755E764AD4467CD7D025761F7D0A17D3DEC1">>},
+ {<<"qdate_localtime">>, <<"5F6C3ACF10ECC5A7E2EFA3DCD2C863102B962188DBD9E086EC01D29FE029DA29">>},
+ {<<"quantile_estimator">>, <<"EF50A361F11B5F26B5F16D0696E46A9E4661756492C981F7B2229EF42FF1CD15">>}]},
+{pkg_hash_ext,[
+ {<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>},
+ {<<"erlware_commons">>, <<"3E7C6FB2BA4C29B0DD5DFE9D031B66449E2088ECEC1A81465BD9FDE05ED7D0DB">>},
+ {<<"qdate_localtime">>, <<"91928E066DA6BCC745FF18B7C368347457CAF9250AD00950E9DA18E129D49EC5">>},
+ {<<"quantile_estimator">>, <<"282A8A323CA2A845C9E6F787D166348F776C1D4A41EDE63046D72D422E3DA946">>}]}
+].
diff --git a/backend/scripts/ci-partial.dockerfile b/backend/scripts/ci-partial.dockerfile
index e37b61c1..7a8947a8 100644
--- a/backend/scripts/ci-partial.dockerfile
+++ b/backend/scripts/ci-partial.dockerfile
@@ -1,4 +1,4 @@
-FROM plazaproject/ci-base-backend:1a433d7f94bb7bbe2343fba4bde2b8e6e2d09683
+FROM programakerproject/ci-base-backend:5e91faa7bb0593d8da302e8ee621871a1be56ccf
ADD . /app
RUN sh -x -c 'if [ ! -f config/sys.config ]; then cp -v config/sys.config.orig config/sys.config ; fi'
diff --git a/backend/scripts/container_init.sh b/backend/scripts/container_init.sh
index c451b58a..00512b4d 100755
--- a/backend/scripts/container_init.sh
+++ b/backend/scripts/container_init.sh
@@ -11,7 +11,7 @@ if [ ! -z "${NODE_NAME_SUFFIX}" ];then
nodename="`hostname`${NODE_NAME_SUFFIX}"
echo "NodeName: backend@$nodename"
- sed 's/^-sname.*$/-name '"backend\@$nodename"'/' -i "${VM_ARGS_PATH}"
+ sed 's/^-sname.*$/-name '"backend\\@$nodename"'/' -i "${VM_ARGS_PATH}"
else
sed 's/^-sname.*$/-sname '"backend"'/' -i "${VM_ARGS_PATH}"
fi
diff --git a/backend/scripts/kubernetes_hotload_module.sh b/backend/scripts/kubernetes_hotload_module.sh
new file mode 100644
index 00000000..98ce08e7
--- /dev/null
+++ b/backend/scripts/kubernetes_hotload_module.sh
@@ -0,0 +1,47 @@
+if [ -z "$3" ]
+then
+ echo "Usage: $0 [ ...]"
+ exit 0
+fi
+
+echo "$1"|grep -q '.erl$'
+if [ $? -ne 0 ]
+then
+ echo "Error: Module argument must be an .erl file"
+ exit 1
+fi
+
+set -e
+
+SOURCE_NAME="$1"
+MODULE_NAME=$(basename "$1"|sed 's/.erl$//')
+COMPILED_FILE=$MODULE_NAME.beam
+K8S_NAMESPACE="$2"
+
+echo "= Compiling..."
+erl -noinput -noshell -eval 'io:fwrite("Compiling: ~p~n", [compile:file("'"$SOURCE_NAME"'")]),erlang:halt().'
+
+if [ ! -f "$COMPILED_FILE" ]
+then
+ echo "Error: Not found expected result file"
+ exit 2
+fi
+
+
+# Skip two first arguments to expose the pod names
+shift 2
+
+for pod in "$@"
+do
+ echo ""
+ echo "Pod: $pod"
+ echo "= Uploading..."
+ kubectl -n "$K8S_NAMESPACE" cp "$COMPILED_FILE" "$pod:/tmp"
+
+ echo " Checking..."
+ kubectl -n "$K8S_NAMESPACE" exec "$pod" -- ls /tmp/"$COMPILED_FILE" >/dev/null
+
+ # Here comes the trick...
+ echo "= Loading..."
+ kubectl -n "$K8S_NAMESPACE" exec "$pod" -- ash -xc "/app/scripts/run_erl.sh 'code:load_abs(\"/tmp/$MODULE_NAME\")' && /app/scripts/run_erl.sh 'code:soft_purge($MODULE_NAME)'"
+done
diff --git a/backend/scripts/kubernetes_remote_shell.sh b/backend/scripts/kubernetes_remote_shell.sh
index 78990649..682a01f1 100755
--- a/backend/scripts/kubernetes_remote_shell.sh
+++ b/backend/scripts/kubernetes_remote_shell.sh
@@ -1,6 +1,14 @@
#!/bin/sh
VM_ARGS_PATH=/app/release/releases/0.0.1/vm.args
-cookie=`grep '^-setcookie' "${VM_ARGS_PATH}"`
+cookie=`grep '^-setcookie' "${VM_ARGS_PATH}"|sed -r 's/^-setcookie +("([^"]+)"|([^ ]+)) *$/\2\3/'`
+nametype=`grep -Eo '^-s?name' "${VM_ARGS_PATH}"`
+
+if [ -z "$nametype" ];then
+ echo "Error: Cannot get name type from ${VM_ARGS_PATH}" 2>>/dev/null
+ echo " No '-sname' or '-name' line found." 2>>/dev/null
+ exit 2
+fi
+
echo 'Write `nodes()` to show where are you connected to.'
-erl $cookie -hidden -name dummy-$RANDOM -remsh "backend@`hostname -f`"
\ No newline at end of file
+erl -setcookie "$cookie" $nametype remote$RANDOM$RANDOM$RANDOM -hidden -remsh "backend@`hostname -f`"
diff --git a/backend/scripts/run_erl.sh b/backend/scripts/run_erl.sh
new file mode 100755
index 00000000..c3519f0a
--- /dev/null
+++ b/backend/scripts/run_erl.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+usage() {
+ echo "Usage: $0 " >&2
+ }
+
+if [ -z "$1" ];then
+ usage
+ exit 1
+fi
+
+VM_ARGS_PATH=/app/release/releases/0.0.1/vm.args
+cookie=`grep '^-setcookie' "${VM_ARGS_PATH}"|sed -r 's/^-setcookie +("([^"]+)"|([^ ]+)) *$/\2\3/'`
+nametype=`grep -Eo '^-s?name ' "${VM_ARGS_PATH}"`
+
+node=backend@`hostname -f`
+
+erl -setcookie "$cookie" -hidden $nametype "dummy-$RAND@`hostname -f`" -remsh "$node" \
+ -eval "io:fwrite(\"~p~n\", [erpc:call('$node', fun() -> $1 end)]), erlang:halt()." \
+ -noinput # Setting the nametype is required for `-noinput` to work correctly
diff --git a/docker-compose.yml b/docker-compose.yml
index c3de3aa4..a40653a7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,28 +2,27 @@
version: '3'
services:
- plaza-router:
+ programaker-router:
build: utils/router
ports:
- 8080:80
links:
- - plaza-frontend
- - plaza-backend
+ - programaker-frontend
+ - programaker-backend
environment:
- - FRONTEND_NODE=plaza-frontend
+ - FRONTEND_NODE=programaker-frontend
- FRONTEND_PORT=80
- - BACKEND_NODE=plaza-backend
+ - BACKEND_NODE=programaker-backend
- BACKEND_PORT=8888
- plaza-frontend:
+ programaker-frontend:
build: frontend
- plaza-backend:
+ programaker-backend:
build: backend
- hostname: plaza-backend-single
+ hostname: programaker-backend-single
volumes:
- - plaza-dev-backend-mnesia:/app/mnesia
+ - programaker-dev-backend-mnesia:/app/mnesia
volumes:
- plaza-dev-backend-mnesia:
-
+ programaker-dev-backend-mnesia:
diff --git a/docs/api/insomnia.yaml b/docs/api/insomnia.yaml
index 5dd43807..dbd05fec 100644
--- a/docs/api/insomnia.yaml
+++ b/docs/api/insomnia.yaml
@@ -38,9 +38,9 @@ resources:
_type: request_group
- _id: wrk_345ba95d0c964d4b85b64df4cc2d0f19
created: 1550782382008
- description: Plaza project API
+ description: Programaker project API
modified: 1550783380599
- name: Plaza
+ name: Programaker
parentId: null
_type: workspace
- _id: req_b0df3c8bd7b7488b85d062ad85c831ba
@@ -237,7 +237,7 @@ resources:
isPrivate: false
metaSortKey: 1
modified: 1559599978615
- name: Plaza local development
+ name: Programaker local development
parentId: env_1ad291d9e0bd469c9992c925c695d3c3
_type: environment
- _id: env_5a872eacd5d74e8fb30138b046d6d430
@@ -259,6 +259,6 @@ resources:
isPrivate: false
metaSortKey: 2
modified: 1559599978619
- name: Plaza local dev. host 2
+ name: Programaker local dev. host 2
parentId: env_1ad291d9e0bd469c9992c925c695d3c3
_type: environment
diff --git a/docs/bridge-communication-protocol.md b/docs/bridge-communication-protocol.md
deleted file mode 100644
index 133d2c73..00000000
--- a/docs/bridge-communication-protocol.md
+++ /dev/null
@@ -1,349 +0,0 @@
-# Bridge communication protocol
-
-**Warning**: This protocol is in development and most of it **will change** later!
-
-This document defines the protocol that a bridge has to implement to expose its functionalities on the Plaza platform. There's four main contexts for this protocol:
-
-1. Bridge initialization
-2. Event notification
-3. Function calls
-4. Service registration
-5. Data callback execution
-
-All communication is performed through the bridge websocket connection to the bridge `communication` endpoint, encoded as JSON. **Note that messages must be sent in a single websocket frame, as fragmentation is not supported.**
-
-## Bridge initialization
-
-As a bridge websocket connects to the platform it must send the following information (JSON encoded):
-
-```json
-{
- "type": "CONFIGURATION",
- "value": {
- "blocks": [ ],
- "is_public": false,
- "service_name": ""
- }
-}
-```
-
-For example, a minimal configuration, with no blocks, might be the following (send it in a single line):
-
-```json
-{ "type": "CONFIGURATION", "value": { "blocks": [], "is_public": false, "service_name": "comm-test" } }
-```
-
-Expanded:
-```json
-{
- "type": "CONFIGURATION",
- "value": {
- "blocks": [],
- "is_public": false,
- "service_name": "comm-test"
- }
-}
-```
-
-Opening a websocket and sending this information will show the bridge as connected, but no blocks will be defined.
-
-Blocks definitions can be of 2 main groups:
-* Operations and Getters. Are called when a program runs then, expecting a return value.
-* Triggers. Are run proactively by the bridge, and triggered by sending event notifications
-
-### Operations and getters
-
-Operation and getter blocks are defined through the following objects
-
-```json
-{
- "id": "",
- "function_name": "",
- "block_type": "",
- "block_result_type": null,
- "message": "",
- "arguments": [ ]
- }
-```
-
-
-* `id` and `function_name`: Define the unique `id` given by the bridge to the operation. **Note** that the internal id operation is duplicated on the `id` and function name. This is a known problem and is something to be fixed. It's recommended to use the the same value on `id` and `function_name` for clarity.
-* `block_type`: Define the nature of the block, can be of two types:
- * `operation`, defined on Scratch as [Stack Block](https://en.scratch-wiki.info/wiki/Stack_Block), can be concatenated.
- * `getter`, defined on Scratch as [Reporter Block](https://en.scratch-wiki.info/wiki/Reporter_Block), can be used in place of a value.
-* `block_result_type` is reserved and not used yet, set it to `null`.
-* `message`: The message shown on the block. All block arguments **must** be present on the message represented as `%` (index starts on 0).
-* `arguments`: the arguments that the operation considers to perform an operation.
-
-#### Arguments
-
-A bridge might require knowing the values of the context to perform a certain operation, these can be passed through arguments, there are 3 types of arguments:
-* Values: Normal values which have been resolved.
-* Variable names: The name of an **unresolved** variable.
-* Selections: A single option of a dynamic list provided by the bridge during "design" time.
-
-##### Values
-
-Simple value arguments can be defined with the following JSON object
-
-```json
-{
- "type": "",
- "default": ""
-}
-```
-
-The type of the argument defines the UI appearance of the software keyboard shown when selecting the block.
-
-##### Variable names
-
-Variable name arguments can be defined with the following JSON object
-
-```json
-{
- "type": "variable",
- "class": "
-}
-```
-
-The class of the argument defines which variables can the user choses among, normal variables or list variables.
-The main application of these variable names are **Trigger blocks**.
-
-##### Selections
-
-See `Data callback execution` for further understanding of these operations.
-
-#### Example
-
-A bridge simple block operation can be configured using this JSON object:
-
-```json
-{
- "type": "CONFIGURATION",
- "value": {
- "blocks": [
- {
- "id": "max-num",
- "function_name": "max-num",
- "block_type": "getter",
- "block_result_type": null,
- "message": "Max of %1 and %2",
- "arguments": [
- {
- "type": "integer",
- "default": "0"
- },
- {
- "type": "integer",
- "default": "1"
- }
- ]
- }
- ],
- "is_public": false,
- "service_name": "comm-test"
- }
-}
-```
-
-In a single line:
-
-```json
-{ "type": "CONFIGURATION", "value": { "blocks": [ { "id": "max-num", "function_name": "max-num", "block_type": "getter", "block_result_type": null, "message": "Max of %1 and %2", "arguments": [ { "type": "integer", "default": "0" }, { "type": "integer", "default": "1" } ] } ], "is_public": false, "service_name": "comm-test" } }
-```
-
-See **Function calls** for how to implement a bridge that answers these types of blocks.
-
-### Trigger blocks
-
-Trigger blocks represent an operation that is started when a certain event happens, they correspond to Scratch [Hat Blocks](https://en.scratch-wiki.info/wiki/Hat_Block) and are defined through the following JSON schema:
-
-```json
-{
- "id": "",
- "function_name": "",
- "block_type": "trigger",
- "key": "",
- "message": "",
- "arguments": [ ],
- "save_to": ,
- "expected_value":
- }
-```
-
-* `id` and `function_name`: Define the unique `id` given by the bridge to the operation. **Note** that the internal id operation is duplicated on the `id` and function name. This is a known problem and is something to be fixed. It's recommended to use the the same value on `id` and `function_name` for clarity.
-* `block_type`: Define the nature of the block. Trigger blocks must be defined as `trigger`.
-* `key`: The event channel (of the ones from the bridge) where the event will be expected.
-* `message`: The message shown on the block. All block arguments **must** be present on the message represented as `%` (index starts on 0).
-* `arguments`: the arguments that the operation considers to perform an operation.
-* `save_to` references an argument, this argument (must be a variable name), will store the content of the event.
-* `expected_value` references an argument, the value of this argument will be checked against the event `content`.
-
-#### Argument references
-
-Argument references are specified with the following schema:
-
-```json
-{
- "type": "argument",
- "index":
-}
-```
-
-For example, a trigger block might be defined with the following structure:
-
-```json
-{
- "type": "CONFIGURATION",
- "value": {
- "blocks": [
- {
- "id": "temperature-reading",
- "function_name": "temperature-reading",
- "key": "temperature-reading",
- "block_type": "trigger",
- "message": "On temperature reading. Set %1",
- "arguments": [
- {
- "type": "variable",
- "class": "single"
- }
- ],
- "save_to": {
- "type": "argument",
- "index": 0
- },
- "expected_value": null
- }
- ],
- "is_public": false,
- "service_name": "comm-test"
- }
-}
-```
-
-In a single line:
-```json
-{"type": "CONFIGURATION", "value": {"blocks": [{"id": "temperature-reading", "function_name": "temperature-reading", "key": "temperature-reading", "block_type": "trigger", "message": "On temperature reading. Set %1", "arguments": [{"type": "variable", "class": "single"}], "save_to": {"type": "argument", "index": 0}, "expected_value": null}], "is_public": false, "service_name": "comm-test"}}
-```
-
-## Event notification
-
-After the configuration above is loaded, events can be used to trigger the `temperature-reading` block. These events can be sent through the websocket by the bridge at any time and take the following shape:
-
-```json
-{
- "type": "NOTIFICATION",
- "key": "",
- "to_user": ,
- "content": "",
- "value": "",
-}
-```
-* `key` field has to match with the one on the *Trigger block*, for it to be triggered.
-* `to_user` might be a bridge-specific user-Id to send the event to a specific user or `null` for it to be sent to all (registered and authorized) users.
-* The value of the `content` field is the one used for the *Trigger block* `save_to` and `expected_value` operative.
-* The `value` entry might contain all the relevant information of the event. Not that this is saved on the platform, but at this point cannot be accessed from outside.
-
-Thus, a notification can be sent for the configuration sent with a JSON object like this:
-
-```json
-{
- "type": "NOTIFICATION",
- "key": "temperature-reading",
- "to_user": null,
- "content": 9999,
- "value": { "sensor": "hot stuff", "reading": 9999 }
-}
-```
-
-In a single line:
-
-```json
-{ "type": "NOTIFICATION", "key": "temperature-reading", "to_user": null, "content": "9999", "value": { "sensor": "hot stuff", "reading": 9999 } }
-```
-
-## Function calls
-
-On the **Operations and getters** section we defined a block, which can be run by plaza
-```json
-{
- "id": "max-num",
- "function_name": "max-num",
- "block_type": "getter",
- "block_result_type": null,
- "message": "Max of %1 and %2",
- "arguments": [
- {
- "type": "integer",
- "default": "0"
- },
- {
- "type": "integer",
- "default": "1"
- }
- ]
-}
-```
-
-when this happens, a message is sent **to the bridge** by the platform with the following schema:
-
-```json
-{
- "type": "FUNCTION_CALL",
- "message_id": "",
- "value": {
- "function_name": "",
- "argument": [ ]
- },
- "user_id": ""
-}
-```
-
-For example:
-
-```json
-{
- "type":"FUNCTION_CALL",
- "message_id":"30d504f1-bdc6-4602-8f02-f72b29572dde",
- "value": {
- "function_name":"max-num",
- "arguments":[ "5", "7" ]
- },
- "user_id":"08240ff1-8414-4daa-a089-21100cbf8ca1"
-}
-```
-
-After the bridges computes the maximum of `5` and `7`, it must answer with the following format:
-
-```json
-{
- "message_id": "",
- "success": true,
- "result":
-}
-```
-
-In our example (and already in a single line)
-
-```json
-{ "message_id":"30d504f1-bdc6-4602-8f02-f72b29572dde", "success": true, "result": 7}
-```
-
-Note that the `result` field doesn't have to be stringified.
-
-Alternatively, if the operation fails, it can be answered signaling no success, but no error tracing is yet implemented:
-```json
-{
- "message_id": "",
- "success": false
-}
-```
-
-## Service registration
-
-TODO
-
-## Data callback execution
-
-**Note**: this section is yet to be written, as future development will probably include major changes on it's operation and it's not required for most usages.
\ No newline at end of file
diff --git a/docs/communications/sequence/bridges.puml b/docs/communications/sequence/bridges.puml
index 13a46db9..672b9db8 100644
--- a/docs/communications/sequence/bridges.puml
+++ b/docs/communications/sequence/bridges.puml
@@ -1,94 +1,94 @@
' Just the simple path
@startuml happy-path.png
participant program
-participant plaza
+participant backend
participant bridge
-program -> plaza: Run order
-plaza -> bridge: Run order
+program -> backend: Run order
+backend -> bridge: Run order
... bridge computes response ...
-plaza <-- bridge: Result
-program <-- plaza: Result
+backend <-- bridge: Result
+program <-- backend: Result
@enduml
@startuml long-running-operation.png
participant program
-participant plaza
+participant backend
participant bridge
-program -> plaza: Run order
-plaza -> bridge: Run order
+program -> backend: Run order
+backend -> bridge: Run order
... bridge computes response ...
-plaza -> bridge: Still running?
-plaza <-- bridge: Yes
+backend -> bridge: Still running?
+backend <-- bridge: Yes
... bridge still computes response ...
-plaza <-- bridge: Result
-program <-- plaza: Result
+backend <-- bridge: Result
+program <-- backend: Result
@enduml
@startuml failed-operation.png
participant program
-participant plaza
+participant backend
participant bridge
-program -> plaza: Run order
-plaza -> bridge: Run order
+program -> backend: Run order
+backend -> bridge: Run order
... bridge fails ...
-plaza -> bridge: Still running?
-plaza <-- bridge: No
-program <-- plaza: Error
+backend -> bridge: Still running?
+backend <-- bridge: No
+program <-- backend: Error
@enduml
@startuml error-on-connection-to-bridge.png
participant program
-participant plaza
+participant backend
participant bridge
-program -> plaza: Run order
-plaza -> bridge: Run order
+program -> backend: Run order
+backend -> bridge: Run order
... bridge connection fails silently ...
-plaza -X bridge: Still running?
+backend -X bridge: Still running?
... Timeout time passes ...
-program <-- plaza: Error
+program <-- backend: Error
@enduml
@startuml disconnection-to-bridge.png
participant program
-participant plaza
+participant backend
participant bridge
-program -> plaza: Run order
-plaza -> bridge: Run order
+program -> backend: Run order
+backend -> bridge: Run order
... bridge connection fails ...
-plaza -> plaza: Connection failed
-program <-- plaza: Error
+backend -> backend: Connection failed
+program <-- backend: Error
@enduml
@startuml too-long-running-operation.png
participant program
-participant plaza
+participant backend
participant bridge
-program -> plaza: Run order
-plaza -> bridge: Run order
+program -> backend: Run order
+backend -> bridge: Run order
... bridge computes response ...
-plaza -> bridge: Still running?
-plaza <-- bridge: Yes
+backend -> bridge: Still running?
+backend <-- bridge: Yes
... this repeats MaxTimeouts ...
-program <-- plaza: Result
-plaza -> bridge: Cancel
+program <-- backend: Result
+backend -> bridge: Cancel
@enduml
@startuml no-bridge-running.png
participant program
-participant plaza
+participant backend
participant bridge
-program -> plaza: Run order
+program -> backend: Run order
destroy bridge
-program <-- plaza: Error
-@enduml
\ No newline at end of file
+program <-- backend: Error
+@enduml
diff --git a/docs/communications/sequence/registration/chat-connection-establishment.puml b/docs/communications/sequence/registration/chat-connection-establishment.puml
new file mode 100644
index 00000000..bd4b6598
--- /dev/null
+++ b/docs/communications/sequence/registration/chat-connection-establishment.puml
@@ -0,0 +1,48 @@
+@startuml sideloaded-connection-establishment
+actor user
+participant PrograMaker as pm
+participant bridge
+participant ChatService as chat
+
+autonumber
+
+... User goes to "New Connections" panel ...
+user -> pm : Get possible connections
+
+note over pm
+ Looks at possible connections on bridges DB.
+end note
+
+user <-- pm : Possible connection list
+
+... User selects a bridge they want to connect to ...
+
+user -> pm : Prepare to start connection to
+pm -> bridge : User wants to establish connection
+pm <-- bridge : Here is the *registration* form
+user <-- pm : Shows registration form
+
+... User gets registration string ...
+
+user -> chat : Send registration string
+chat -> bridge : Message propagation
+bridge -> pm : Registration with completed, name=X
+
+note over pm
+ - Create connection in DB
+ - Save returned data
+end note
+
+user <- pm : Connection established
+
+note right of user
+ How is this data sent?
+ Polling? Websocket?
+ - For this each "connection attempt" needs an ID
+end note
+
+bridge <-- pm : OK
+chat <-- bridge : Send message "Connection established"
+user <-- chat : Message "Connection established"
+
+@enduml
\ No newline at end of file
diff --git a/docs/communications/sequence/registration/connection-establishment.puml b/docs/communications/sequence/registration/connection-establishment.puml
new file mode 100644
index 00000000..2d2ee878
--- /dev/null
+++ b/docs/communications/sequence/registration/connection-establishment.puml
@@ -0,0 +1,37 @@
+@startuml connection-establishment
+actor user
+participant PrograMaker as pm
+participant bridge
+
+autonumber
+
+... User goes to "New Connections" panel ...
+user -> pm : Get possible connections
+
+note over pm
+ Looks at possible connections on bridges DB.
+end note
+
+user <-- pm : Possible connection list
+
+... User selects a bridge they want to connect to ...
+
+user -> pm : Prepare to start connection to
+pm -> bridge : User wants to establish connection
+pm <-- bridge : Here is the *registration* form
+user <-- pm : Shows registration form
+
+... User completes registration form ...
+
+user -> pm : Answer
+pm -> bridge : Answer
+pm <-- bridge : Success=true, name=X
+
+note over pm
+ - Create connection in DB
+ - Save returned data
+end note
+
+user <-- pm : Connection established
+
+@enduml
\ No newline at end of file
diff --git a/docs/communications/sequence/registration/oauth-connection-establishment-internal.puml b/docs/communications/sequence/registration/oauth-connection-establishment-internal.puml
new file mode 100644
index 00000000..197ca904
--- /dev/null
+++ b/docs/communications/sequence/registration/oauth-connection-establishment-internal.puml
@@ -0,0 +1,41 @@
+@startuml oauth-connection-establishment-internal
+actor user
+participant PrograMaker as pm
+participant bridge
+participant OAuthService as oauth
+
+autonumber
+
+... User goes to "New Connections" panel ...
+user -> pm : Get possible connections
+
+note over pm
+ Looks at possible connections on bridges DB.
+end note
+
+user <-- pm : Possible connection list
+
+... User selects a bridge they want to connect to ...
+
+user -> pm : Prepare to start connection to
+pm -> bridge : User wants to establish connection
+pm <-- bridge : Here is the *registration* form
+user <-- pm : Shows registration form
+
+... User gets registration URL ...
+
+user -> oauth : Access & Identify
+user <-- oauth : Redirect with token
+
+user -> pm : Register with token
+pm -> bridge : Token
+pm <-- bridge : Success=true, name=X
+
+note over pm
+ - Create connection in DB
+ - Save returned data
+end note
+
+user <-- pm : Connection established
+
+@enduml
\ No newline at end of file
diff --git a/docs/diagrams/.gitignore b/docs/diagrams/.gitignore
new file mode 100644
index 00000000..dc6b97b3
--- /dev/null
+++ b/docs/diagrams/.gitignore
@@ -0,0 +1,3 @@
+db-model.png
+db-model.svg
+
diff --git a/docs/diagrams/db-model.gv b/docs/diagrams/db-model.gv
index 030e009e..49bf0c12 100644
--- a/docs/diagrams/db-model.gv
+++ b/docs/diagrams/db-model.gv
@@ -1,110 +1,201 @@
digraph g {
- rankdir=LR
- node[shape=record];
- style=dashed;
+ rankdir=LR
+ node[shape=record];
+ style=dashed;
+
+ subgraph cluster_legend {
+ label="Legend"
+
+ storage;
+ channels[style=filled,fillcolor="#bbffff"];
+ coordination[style=filled,fillcolor="#ffbbbb"]
+ bridge_engine[style=filled,fillcolor="#bbbbff"];
+ service_registry[style=filled,fillcolor="#bbffbb"]
+ templates[style=filled,fillcolor="#ffbbff"]
+ }
+
+
+ subgraph group_core_channels {
+ label="Channels";
+ node[style=filled,fillcolor="#bbffff"];
+
+ // LIVE_CHANNELS_TABLE | automate_channel_engine_live_channels_table
+ live_channels_table_entry[label="*Channel* | id | stats"];
+
+ // LISTENERS_TABLE | automate_channel_engine_listeners_table
+ listeners_table_entry[label="*Listener*| channel_id | pid | node | key | subkey"];
+ listeners_table_entry -> live_channels_table_entry:pk;
+
+ // MONITORS_TABLE | automate_channel_engine_monitors_table
+ monitors_table_entry[label="*Monitor* | live_channel_id | pid | node"];
+ monitors_table_entry:f0 -> live_channels_table_entry:pk;
+ }
+
+ subgraph group_core_storage {
+ label="Storage";
+
+ // REGISTERED_USERS_TABLE | automate_registered_users
+ registered_user_entry[label="*Registered user* | id | username | password | email | status | registration_time | is_admin | is_advanced | is_in_preview"];
+
+ // User group
+ user_group[
+ color="green",
+ label="*User group* | id | name | canonical_name | public"]
+
+ // User in group
+ user_in_group[color=green,
+ label="*User in group* | group_id | user_id | role"]
+ user_in_group:f0 -> user_group:pk
+ user_in_group:f1 -> registered_user_entry:pk[style=bold,color="#ff0000"];
+
+ // User in group invitation
+ user_in_group_invitation[color=green,
+ label="*User in group invitation* | group_id | user_id | role"]
+ user_in_group_invitation:f0 -> user_group:pk
+ user_in_group_invitation:f1 -> registered_user_entry:pk[style=bold,color="#ff0000"];
+
+ // User or group
+ owner[color=green,style=dashed,
+ label="*Owner* | id | type (user/group) | user_id / group_id"]
+ owner:f1 -> user_group:pk
+ owner:f1 -> registered_user_entry:pk[style=bold,color="#ff0000"];
+
+ // USER_SESSIONS_TABLE | automate_user_sessions
+ user_session_entry[style=dashed, // Maybe a permisisons-based model would be interesting
+ label="*User session* | session_id | user_id | session_start_time | session_last_used_time"];
+ user_session_entry:f0 -> registered_user_entry:pk[style=dashed,color="#ff0000"];
+
+ // // USER_MONITORS_TABLE | automate_user_monitors
+ monitor_entry[color=red, // Deprecated
+ label="*Monitor entry* [Dep] | id | owner | type | name | value"];
+ monitor_entry:f0 -> owner:pk[style=normal,color="#0000ff"];
+
+ // USER_PROGRAMS_TABLE | automate_user_programs
+ user_program_entry[label="*Program* | id | owner | program_name | program_type | program_parsed | program_orig | enabled | program_channel | creation_time | last_upload_time | last_successful_call_time | last_failed_call_time"];
+ user_program_entry:f0 -> owner:pk[style=normal,color="#0000ff"];
+ user_program_entry:f1 -> live_channels_table_entry:pk;
+
+ // USER_PROGRAMS_LOGS_TABLE | automate_user_program_logs
+ user_program_log_entry[label="*Log line* | program_id | thread_id | owner | block_id | event_data | event_message | event_time | severity | exception_data"];
+ user_program_log_entry:f0 -> user_program_entry:pk;
+ user_program_log_entry:f2 -> owner:pk[style=normal,color="#0000ff"];
+
+ // USER_GENERATED_LOGS_TABLE | automate_user_generated_logs
+ user_generated_log_entry[label="*User generated log* | program_id | block_id | severity | event_time | event_message"]
+ user_generated_log_entry:f0 -> user_program_entry:pk;
+
+ // USER_PROGRAM_EVENTS_TABLE | automate_user_program_events
+ user_program_editor_event[label="*User editor event* | program_id | event | event_tag"]
+ user_program_editor_event:f0 -> user_program_entry:pk;
+
+ // USER_PROGRAM_CHECKPOINTS_TABLE | automate_user_program_checkpoints
+ user_program_checkpoint[label="*Program checkpoint* | program_id | user_id | event_time | content"]
+ user_program_checkpoint:f0 -> user_program_entry:pk;
+ user_program_checkpoint:f1 -> registered_user_entry:pk[style=bold,color="#ff0000"];
+
+ // PROGRAM_TAGS_TABLE | automate_program_tags
+ program_tags_entry[label="*Program tags* | program_id | tags"];
+ program_tags_entry:f0 -> user_program_entry:pk;
+
+ // RUNNING_PROGRAMS_TABLE | automate_running_programs
+ running_program_entry[label="*Running program*| program_id | runner_pid | variables | stats"];
+ running_program_entry:f0 -> user_program_entry:pk;
+
+ // RUNNING_THREADS_TABLE | automate_running_program_threads
+ running_program_thread_entry[label="*Program thread* | thread_id | runner_pid | parent_program_id | instructions | memory | instruction_memory | position | stats"];
+ user_program_logs_entry:f1 -> running_program_thread_entry:pk;
+ running_program_thread_entry:f0 -> user_program_entry:pk;
+
+ // PROGRAM_VARIABLE_TABLE | automate_program_variable_table
+ program_variable_table_entry[label="*Program variable* | { program_id | var_name} | value"];
+ program_variable_table_entry:f0 -> user_program_entry:pk; // Not sure if user program or running program
+
+ // CUSTOM_SIGNALS_TABLE | automate_custom_signals_table
+ custom_signal_entry[label="*Custom signal* | id | name | owner"];
+ custom_signal_entry:f0 -> owner:pk[style=normal,color="#0000ff"];
+
+ // INSTALLATION_CONFIGURATION_TABLE | automate_installation_configuration
+ storage_configuration_entry[label="*Installation configuration* | id | value"];
+
+ // USER_VERIFICATION_TABLE | automate_user_verification_table
+ user_verification_entry[label="*User verification* | id | user_id | verification_type"]
+ user_verification_entry:f0 -> registered_user_entry:pk[style=bold,color="#ff0000"];
+ }
+
+ subgraph group_coordination {
+ label="Coordination";
+ node[style=filled,fillcolor="#ffbbbb"];
+
+ // RUN_ONCE_TASKS_TABLE | automate_coordination_run_once_tasks
+ run_once_tasks_table_entry[label="*Run once tasks* | task_id | node | pid"]
+ }
+
+ subgraph group_registry_services {
+ label="Service registry";
+ node[style=filled,fillcolor="#bbffbb"]
+
+ // SERVICE_REGISTRY_TABLE | automate_service_registry_services_table
+ services_table_entry[label="*Service* | id | public? | name | description | module"];
+
+ // USER_SERVICE_ALLOWANCE_TABLE | automate_service_registry_user_service_allowance_table
+ user_service_allowance_entry[label="*Allowed service*| service_id | user_id"];
+ user_service_allowance_entry:f0 -> services_table_entry:pk;
+ user_service_allowance_entry:f1 -> owner:pk[style=normal,color="#0000ff"];
+
+ // SERVICE_CONFIGURATION_TABLE | automate_service_registry_service_configuration_table
+ service_configuration_entry[label="*Service configuration* | { service_id | key } | value "];
+ service_configuration_entry:f0 -> services_table_entry:pk;
+ }
+
+ subgraph group_bridges {
+ label="Bridge engine";
+
+ node[style=filled,fillcolor="#bbbbff"]
+
+ // SERVICE_PORT_TABLE | automate_service_port_table
+ service_port_entry[label="*Bridge* | id | name | owner | service_id"]
+ service_port_entry:f0 -> owner:pk[style=normal,color="#0000ff"];
+ service_port_entry:f1 -> services_table_entry:pk;
+
+ // SERVICE_PORT_CONFIGURATION_TABLE | automate_service_port_configuration_table
+ service_port_configuration[label="*Bridge config* | id | service_name | service_id | is_public | blocks | icon | allow_multiple_connections"]
+ service_port_configuration:pk -> service_port_entry:pk;
+ service_port_configuration:f0 -> services_table_entry:pk
+
+
+ // SERVICE_PORT_CHANNEL_TABLE | automate_service_port_channel_table
+ service_port_monitor_channel_entry[label="*Bridge channel* | { owner | bridge_id } | channel_id"]
+ service_port_monitor_channel_entry:f0 -> owner:pk[style=normal,color="#0000ff"];
+ service_port_monitor_channel_entry:f1 -> service_port_entry:pk;
+ service_port_monitor_channel_entry:f2 -> live_channels_table_entry:pk;
- subgraph cluster_core {
- label="Core";
+ // SERVICE_PORT_CHANNEL_MONITORS_TABLE | automate_service_port_channel_monitors_table
+ channel_monitor_table_entry[label="*Monitor table* | { bridge_id} | pid | node"]
+ channel_monitor_table_entry:f0 -> service_port_entry:pk
- subgraph cluster_core_storage {
- label="Storage";
+ // USER_TO_BRIDGE_CONNECTION_TABLE | automate_service_port_channel_user_to_bridge_connection_table // Bridge connection
+ user_to_bridge_connection_entry[
+ label="*Connection* | id | bridge_id | user_id | channel_id | name | creation_time"]
+ user_to_bridge_connection_entry:f0 -> service_port_entry:pk
+ user_to_bridge_connection_entry:f1 -> owner:pk[style=normal,color="#0000ff"];
+ user_to_bridge_connection_entry:f2 -> live_channels_table_entry:pk
+ // Connection data might store data from the bridges
- // REGISTERED_USERS_TABLE | automate_registered_users
- registered_user_entry[label="*Registered user* | id | username | password | email"];
+ pending_connection_entry[label="*Pending connection* | id | bridge_id | owner | channel_id | creation_time"]
+ pending_connection_entry:f0 -> service_port_entry:pk
+ pending_connection_entry:f1 -> owner:pk[style=normal,color="#0000ff"];
+ pending_connection_entry:f2 -> live_channels_table_entry:pk
- // USER_SESSIONS_TABLE | automate_user_sessions
- user_session_entry[label="*User session* | session_id | user_id | session_start_time"];
- user_session_entry:f0 -> registered_user_entry:pk;
+ }
- // USER_MONITORS_TABLE | automate_user_monitors
- monitor_entry[label="*Monitor entry* | id | user_id | type | name | value"];
+ subgraph group_core_template_engine {
+ label="Template engine"
+ node[style=filled,fillcolor="#ffbbff"]
- monitor_entry:f0 -> registered_user_entry:pk;
+ // TEMPLATE_TABLE | automate_template_engine_templates_table
+ template_entry[label="*Template* | id | name | owner | content"]
+ template_entry:f0 -> owner:pk[style=normal,color="#0000ff"];
- // USER_PROGRAMS_TABLE | automate_user_programs
- user_program_entry[label="*Program* | id | user_id | program_name | program_type | program_parsed | program_orig"];
- user_program_entry:f0 -> registered_user_entry:pk;
-
- // RUNNING_PROGRAMS_TABLE | automate_running_programs
- running_program_entry[label="*Running program*| program_id | runner_pid | variables | stats"];
- running_program_entry:f0 -> user_program_entry:pk;
-
- // REGISTERED_SERVICES_TABLE | automate_registered_services
- registered_service_entry[label="*Service registration* | registration_id | service | user_id | enabled"];
- registered_service_entry:f1 -> registered_user_entry:pk;
-
- // PROGRAM_VARIABLE_TABLE | automate_program_variable_table
- program_variable_table_entry[label="*Program variable* | program_id + var_name | value"];
- program_variable_table_entry:f0 -> user_program_entry:pk; // Not sure if user program or running program
- }
-
- subgraph cluster_core_channels {
- label="Channels";
-
- // LIVE_CHANNELS_TABLE | automate_channel_engine_live_channels_table
- live_channels_table_entry[label="*Channel* | id | stats"];
-
- // LISTENERS_TABLE | automate_channel_engine_listeners_table
- listeners_table_entry[label="*Listener*| channel_id | pid"];
- listeners_table_entry -> live_channels_table_entry:pk;
- }
-
- subgraph cluster_core_chats {
- label="Chats";
-
- // CHAT_HANDLER_MODULE_TABLE | automate_chat_handler_module_table
- chat_handler_module_entry[label="*Chat* | prefix_id | handler_module"];
- }
-
- subgraph cluster_core_services {
- label="Services";
-
- // SERVICE_REGISTRY_TABLE | automate_service_registry_services_table
- services_table_entry[label="*Service* | id | public? | name | description | module"];
-
- // USER_SERVICE_ALLOWANCE_TABLE | automate_service_registry_user_service_allowance_table
- user_service_allowance_entry[label="*Allowed service*| service_id | user_id"];
- user_service_allowance_entry:f0 -> services_table_entry:pk;
- user_service_allowance_entry:f1 -> registered_user_entry:pk;
-
- // SERVICE_CONFIGURATION_TABLE | automate_service_registry_service_configuration_table
- service_configuration_entry[label="*Service configuration* | service_id + key | value "];
- service_configuration_entry:pk -> services_table_entry:pk;
- }
-
- subgraph cluster_core_user_service_registration {
- label="User registration in service";
-
- // SERVICE_REGISTRATION_TOKEN_TABLE | automate_service_registration_token_table
- service_registration_token[label="*Registration session* | token | service_id | user_id"];
- service_registration_token:f0 -> services_table_entry:pk;
- service_registration_token:f1 -> registered_user_entry:pk;
- }
- }
-
- subgraph cluster_services {
- label="Services";
-
- subgraph cluster_services_telegram {
- label="Telegram";
-
- // TELEGRAM_SERVICE_REGISTRATION_TABLE | automate_telegram_service_registration_table
- telegram_service_registration_entry[label="*Telegram users* | telegram user id | internal user id"];
- telegram_service_registration_entry:f0 -> registered_user_entry:pk;
-
- // TELEGRAM_SERVICE_CHATS_KNOWN_TABLE | automate_telegram_service_chats_known_table
- telegram_service_known_chat_entry[label="*Telegram chats*| chat_id | chat_name"];
-
- // TELEGRAM_SERVICE_USER_CHANNEL_TABLE | automate_telegram_service_user_channel_table
- telegram_service_user_channel_entry[label="*Users in channel*| internal user id| channel id"];
- telegram_service_user_channel_entry:f0 -> registered_user_entry:pk;
- telegram_service_user_channel_entry:f1 -> live_channels_table_entry:pk;
-
- // TELEGRAM_SERVICE_CHATS_MEMBERS_TABLE | automate_telegram_service_chats_members_table
- telegram_service_chat_member_entry[label="*Users in channel*| internal user id| chat id"];
- telegram_service_chat_member_entry:f0 -> registered_user_entry:pk;
- telegram_service_chat_member_entry:f1 -> telegram_service_known_chat_entry:pk;
- }
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/docs/test-table.csv b/docs/test-table.csv
new file mode 100644
index 00000000..d3ccc1e8
--- /dev/null
+++ b/docs/test-table.csv
@@ -0,0 +1,109 @@
+Operations,Requires,Generates,,Users,,,,,,
+,,,,,,,,,,
+Misc,,,,Admin,User,Anonymous,,,,
+"/metrics [skip]",,,,GET,,,,,,
+/ping,,,,GET,GET,GET,,,,
+/utils/autocomplete/users,,,,IGNORE,GET,,,,,
+,,,,,,,,,,
+Assets,,,,Admin,User,Anonymous,,,,
+"/assets/icons/ [skip]",,,,GET,GET,,,,,
+,,,,,,,,,,
+Administration,,,,Admin,User,Anonymous,,,,
+/admin/stats,,,,GET,,,,,,
+,,,,,,,,,,
+Registration,,,,Admin,User,Anonymous,,,,
+/sessions/register,,UserData,,,,POST,,,,
+"/sessions/register/verify [skip]",UserToken,User,,,,POST,,,,
+"/sessions/login/reset [skip]",,ResetToken,,POST,POST,POST,,,,
+"/sessions/login/reset/validate [skip]",ResetToken,,,POST,POST,POST,,,,
+"/sessions/login/reset/update [skip]",ResetToken,,,POST,POST,POST,,,,
+,,,,,,,,,,
+"Session management",,,,Admin,User,Anonymous,,,,
+/sessions/check,,,,IGNORE,GET,,,,,
+/sessions/login,UserData,"User, Login",,IGNORE,IGNORE,POST,,,,
+,,,,,,,,,,
+Users,,,,Admin,User,Anonymous,,,,
+/users,,,,GET,,,,,,
+"/users/:user_id [skip]",User,,,,,,,,,
+"/users/by-id/:user_id/picture [skip]",User,Picture,,GET,"GET, POST",GET,,,,
+,,,,,,,,,,
+Items,,,,Admin,User,Anonymous,,,,
+/users/id/:user_id/custom_signals,User,Signal,,,"GET, POST",,,,,
+/users/id/:user_id/groups,User,,,,GET,,,,,
+/users/id/:user_id/templates,User,Template,,,"GET, POST",,,,,
+/users/id/:user_id/templates/id/:template_id,User,,,,"GET, PUT, DELETE",,,,,
+/users/:user_name/custom-blocks,User,,,,GET,,,,,
+/users/id/:user_id/settings,User,,,,POST,,,,,
+,,,,,,,,,,
+"Old Programs Section",,,,Admin,User,Anonymous,,,,
+/users/:user_name/programs,User,Program,,,"GET, POST",,,,,
+/users/id/:user_id/programs/id/:program_id/checkpoint,"User, Program",,,,POST,,,,,
+/users/id/:user_id/programs/id/:program_id/communication,"User, Program",,,,READ,,,,,
+/users/id/:user_id/programs/id/:program_id/logs-stream,"User, Program",,,,READ,,,,,
+/users/id/:user_id/programs/id/:program_id/editor-events,"User, Program",,,,"READ, WRITE",,,,,
+/users/id/:user_id/programs/id/:program_id/logs,"User, Program",,,,GET,,,,,
+/users/id/:user_id/programs/id/:program_id/tags,"User, Program",,,,"GET, POST",,,,,
+/users/id/:user_id/programs/id/:program_id/stop-threads,"User, Program",,,,POST,,,,,
+/users/id/:user_id/programs/id/:program_id/status,"User, Program",,,,POST,,,,,
+,,,,,,,,,,
+Programs,,,,Admin,User,Anonymous,,"G. Admin","G. Editor","G. Viewer"
+/programs/by-id/:program_id,Program,,,,"GET, PUT, PATCH, DELETE",,,"GET, PUT, PATCH, DELETE","GET, PUT, PATCH, DELETE",GET
+/programs/by-id/:program_id/checkpoint,Program,,,,POST,,,POST,POST,
+/programs/by-id/:program_id/logs-stream,Program,,,,READ,,,READ,READ,
+/programs/by-id/:program_id/editor-events,Program,,,,"READ, WRITE",,,"READ, WRITE","READ, WRITE",READ
+/programs/by-id/:program_id/custom-blocks,Program,,,,GET,,,GET,GET,GET
+/programs/by-id/:program_id/bridges/by-id/:bridge_id/callbacks/:callback,Program,,,,GET,,,GET,GET,GET
+/programs/by-id/:program_id/logs,Program,,,,GET,,,GET,GET,
+/programs/by-id/:program_id/tags,Program,,,,"GET, POST",,,"GET, POST","GET, POST",GET
+"/programs/by-id/:program_id/stop-threads ",Program,,,,POST,,,POST,POST,
+/programs/by-id/:program_id/status,Program,,,,POST,,,POST,POST,
+,,,,,,,,,,
+,,,,,,,,,,
+Connections,,,,Admin,User,Anonymous,,"G. Admin","G. Editor","G. Viewer"
+/users/id/:user_id/connections/available,User,,,,GET,,,IGNORE,IGNORE,IGNORE
+"/users/id/:user_id/connections/established [skip]",User,,,,"GET, POST",,,IGNORE,IGNORE,IGNORE
+/users/id/:user_id/connections/pending/:connection_id/wait,"User, Connection",,,,READ,,,IGNORE,IGNORE,IGNORE
+,,,,,,,,,,
+/groups/by-id/:group_id/connections/available,Group,,,,IGNORE,,,GET,GET,
+/groups/by-id/:group_id/connections/established,Group,,,,IGNORE,,,GET,GET,GET
+/groups/by-id/:group_id/connections/pending/:connection_id/wait,"Group, Connection",,,,IGNORE,,,READ,READ,
+,,,,,,,,,,
+"/programs/by-id/:program_id/connections/established [skip]",Program,,,,GET,,,GET,GET,GET
+,,,,,,,,,,
+Bridges,,,,Admin,User,Anonymous,,,,
+/users/:user_name/bridges,User,Bridge,,,"GET, POST",,,,,
+/users/id/:user_id/bridges,User,Bridge,,,"GET, POST",,,,,
+/users/id/:user_id/bridges/id/:bridge_id,"User, Bridge",,,,DELETE,,,,,
+/users/id/:user_id/bridges/id/:bridge_id/callback/:callback,"User, Bridge, Callback",,,,GET,,,,,
+/users/id/:user_id/bridges/id/:bridge_id/functions/:function,"User, Bridge, Function",,,,POST,,,,,
+/users/id/:user_id/bridges/id/:bridge_id/signals,"User, Bridge",,,,READ,,,,,
+/users/id/:user_id/bridges/id/:bridge_id/signals/:key,"User, Bridge",,,,READ,,,,,
+,,,,,,,,,,
+/users/id/:user_id/bridges/id/:bridge_id/communication,"User, Bridge",,,,,,,,,
+/users/id/:user_id/bridges/id/:bridge_id/oauth_return,"User, Bridge",,,,,,,,,
+,,,,,,,,,,
+"Old Services section",,,,Admin,User,Anonymous,,,,
+/users/:user_name/services,User,,,,GET,,,,,
+/users/:user_name/services/id/:service_id/how-to-enable,"User, Service",,,,GET,,,,,
+/users/:user_name/services/id/:service_id/register,"User, Service",,,,POST,,,,,
+,,,,,,,,,,
+,,,,,,,,,,
+Services,,,,Admin,User,Anonymous,,"G. Admin","G. Editor","G. Viewer"
+/services/by-id/:service_id/how-to-enable,Service,,,,GET,,,GET,GET,
+/services/by-id/:service_id/register,Service,,,,POST,,,POST,POST,
+,,,,,,,,,,
+/programs/by-id/:program_id/services,Program,,,,GET,,,GET,GET,GET
+,,,,,,,,,,
+Groups,,,,Admin,User,Anonymous,,"G. Admin","G. Editor","G. Viewer"
+/groups,,Group,,IGNORE,POST,,,IGNORE,IGNORE,IGNORE
+/groups/by-name/:group_name,Group,,,,IGNORE,,,GET,GET,GET
+/groups/by-id/:group_id,Group,,,,IGNORE,,,"PATCH, DELETE",,
+/groups/by-id/:group_id/programs,Group,Program,,,IGNORE,,,"GET, POST","GET, POST",GET
+"/groups/by-id/:group_id/collaborators [skip]",Group,,,,IGNORE,,,"GET, POST",GET,GET
+/groups/by-id/:group_id/bridges,Group,,,,IGNORE,,,"GET, POST","GET, POST",GET
+/groups/by-id/:group_id/picture,Group,GroupPicture,,,IGNORE,,,POST,IGNORE,IGNORE
+/groups/by-id/:group_id/picture,"Group, GroupPicture",,,IGNORE,IGNORE,GET,,"POST, GET",GET,GET
+,,,,,,,,,,
+Monitors,,,,Admin,User,Anonymous,,"G. Admin","G. Editor","G. Viewer"
+/users/:user_name/monitors,User,,,,"GET, POST",,,IGNORE,IGNORE,IGNORE
+/programs/by-id/:program_id/monitors,Program,,,,IGNORE,,,GET,GET,GET
diff --git a/frontend/browserslist b/frontend/.browserslistrc
similarity index 100%
rename from frontend/browserslist
rename to frontend/.browserslistrc
diff --git a/frontend/.dockerignore b/frontend/.dockerignore
deleted file mode 120000
index 3e4e48b0..00000000
--- a/frontend/.dockerignore
+++ /dev/null
@@ -1 +0,0 @@
-.gitignore
\ No newline at end of file
diff --git a/frontend/.dockerignore b/frontend/.dockerignore
new file mode 100644
index 00000000..5c7e6f76
--- /dev/null
+++ b/frontend/.dockerignore
@@ -0,0 +1,47 @@
+# See https://docs.docker.com/engine/reference/builder/#dockerignore-file
+
+# Ignore non-applicable changes when ADDing files
+Dockerfile
+.git
+
+# compiled output
+/dist
+/tmp
+out-tsc
+scripts/frontend-browser-ci-partial.dockerfile
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage/*
+/libpeerconnection.log
+npm-debug.log
+testem.log
+/typings
+
+# e2e
+/e2e/*.js
+/e2e/*.map
+
+#System Files
+.DS_Store
+Thumbs.db
diff --git a/frontend/.gitignore b/frontend/.gitignore
index 6c52f646..ba5a5129 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -1,12 +1,10 @@
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file
-# Ignore non-applicable changes when ADDing files
-Dockerfile
-.git
-
# compiled output
/dist
/tmp
+out-tsc
+scripts/frontend-browser-ci-partial.dockerfile
# dependencies
/node_modules
@@ -19,6 +17,7 @@ Dockerfile
*.launch
.settings/
*.sublime-workspace
+.log
# IDE - VSCode
.vscode/*
diff --git a/frontend/Dockerfile.ssr b/frontend/Dockerfile.ssr
new file mode 100644
index 00000000..e78ad7ff
--- /dev/null
+++ b/frontend/Dockerfile.ssr
@@ -0,0 +1,28 @@
+FROM node:lts-alpine as ci-base
+RUN apk add --no-cache make python2 g++
+RUN mkdir /app
+WORKDIR /app
+
+# Build dependencies
+ADD package.json /app
+ADD package-lock.json /app
+RUN npm install --unsafe-perm
+
+# Build final app
+FROM ci-base as builder
+ADD . /app
+RUN make
+RUN npm run build:ssr
+
+# Copy final app to runner
+FROM node:lts-alpine as runner
+
+COPY --from=builder /app/dist /app/dist
+
+WORKDIR app
+
+# Webserver port
+ENV PORT 80
+EXPOSE 80
+
+CMD ["node", "/app/dist/programaker/server/main.js"]
diff --git a/frontend/Makefile b/frontend/Makefile
index 42c2bc76..27f7a4ba 100644
--- a/frontend/Makefile
+++ b/frontend/Makefile
@@ -2,9 +2,9 @@ CSS_LIBS_PATH=src/app/libs/css
FONTS_LIBS_PATH=src/app/libs/fonts
INDEX_LIBS_PATH=src/libs/
-.PHONY: all libs
+.PHONY: all libs assets
-all: libs
+all: libs assets
libs: $(CSS_LIBS_PATH)/bootstrap.min.css \
$(CSS_LIBS_PATH)/material-icons.css \
@@ -12,6 +12,11 @@ libs: $(CSS_LIBS_PATH)/bootstrap.min.css \
$(INDEX_LIBS_PATH)/css/nprogress.css \
$(FONTS_LIBS_PATH)/material-icons.ttf
+assets: src/assets/flow_editor.css
+
+src/assets/flow_editor.css: src/flow_editor.scss
+ node node_modules/sass/sass.js $< > $@
+
$(INDEX_LIBS_PATH)/js/nprogress.js: node_modules/nprogress/nprogress.js
cp -v $< $@
diff --git a/frontend/angular.json b/frontend/angular.json
index 1826902c..e48ab67a 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
- "plaza": {
+ "programaker": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
@@ -11,7 +11,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
- "outputPath": "dist",
+ "outputPath": "dist/programaker/browser",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
@@ -24,6 +24,8 @@
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/nprogress/nprogress.css",
+ "node_modules/huebee/dist/huebee.min.css",
+ "node_modules/ngx-toastr/toastr.css",
"src/styles.scss",
"src/material_theme.scss"
],
@@ -33,14 +35,33 @@
"node_modules/scratch-blocks/msg/js/en.js",
"node_modules/scratch-blocks/blocks_compressed.js",
"node_modules/scratch-blocks/blocks_compressed_vertical.js",
+ "node_modules/fuse.js/dist/fuse.min.js",
"node_modules/nprogress/nprogress.js",
+ "node_modules/huebee/dist/huebee.pkgd.min.js"
]
},
"configurations": {
+ "pm-dev": {
+ "optimization": false,
+ "outputHashing": "all",
+ "sourceMap": true,
+ "extractCss": false,
+ "namedChunks": false,
+ "aot": false,
+ "extractLicenses": false,
+ "vendorChunk": false,
+ "buildOptimizer": false,
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.programaker-dev.ts"
+ }
+ ]
+ },
"programaker": {
"optimization": true,
"outputHashing": "all",
- "sourceMap": false,
+ "sourceMap": true,
"extractCss": true,
"namedChunks": false,
"aot": true,
@@ -57,7 +78,7 @@
"production": {
"optimization": true,
"outputHashing": "all",
- "sourceMap": false,
+ "sourceMap": true,
"extractCss": true,
"namedChunks": false,
"aot": true,
@@ -76,18 +97,21 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
- "browserTarget": "plaza:build"
+ "browserTarget": "programaker:build"
},
"configurations": {
"production": {
- "browserTarget": "plaza:build:production"
+ "browserTarget": "programaker:build:production"
+ },
+ "pm-dev": {
+ "browserTarget": "programaker:build:pm-dev"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
- "browserTarget": "plaza:build"
+ "browserTarget": "programaker:build"
}
},
"test": {
@@ -98,23 +122,33 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [
- "src/app/blocks/blockly_vertical.js",
- "src/app/blocks/blocks.js",
- "src/app/blocks/blocks_vertical.js",
- "src/app/blocks/msg_en.js",
- "src/app/js/workspace.js",
- "src/app/js/initial.js",
- "src/app/js/custom_blocks.js",
- "src/app/js/custom_toolbox.js"
+ "node_modules/scratch-blocks/blockly_compressed_vertical.js",
+ "node_modules/scratch-blocks/msg/messages.js",
+ "node_modules/scratch-blocks/msg/js/en.js",
+ "node_modules/scratch-blocks/blocks_compressed.js",
+ "node_modules/scratch-blocks/blocks_compressed_vertical.js",
+ "node_modules/fuse.js/dist/fuse.min.js",
+ "node_modules/nprogress/nprogress.js",
+ "node_modules/huebee/dist/huebee.pkgd.min.js"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
+ "node_modules/nprogress/nprogress.css",
+ "node_modules/huebee/dist/huebee.min.css",
+ "node_modules/ngx-toastr/toastr.css",
"src/styles.scss",
"src/material_theme.scss"
],
"assets": [
"src/assets",
- "src/favicon.ico"
+ "src/favicon.ico",
+ "src/favicon.png"
+ ],
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.tests.ts"
+ }
]
}
},
@@ -123,14 +157,73 @@
"options": {
"tsConfig": [
"src/tsconfig.app.json",
- "src/tsconfig.spec.json"
+ "src/tsconfig.spec.json",
+ "src/tsconfig.server.json"
],
"exclude": []
}
+ },
+ "server": {
+ "builder": "@angular-devkit/build-angular:server",
+ "options": {
+ "outputPath": "dist/programaker/server",
+ "main": "server.ts",
+ "tsConfig": "src/tsconfig.server.json"
+ },
+ "configurations": {
+ "production": {
+ "outputHashing": "media",
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "sourceMap": false,
+ "optimization": true
+ },
+ "programaker": {
+ "outputHashing": "media",
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.programaker.ts"
+ }
+ ],
+ "sourceMap": true,
+ "optimization": true
+ }
+ }
+ },
+ "serve-ssr": {
+ "builder": "@nguniversal/builders:ssr-dev-server",
+ "options": {
+ "browserTarget": "programaker:build",
+ "serverTarget": "programaker:server"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "programaker:build:production",
+ "serverTarget": "programaker:server:production"
+ }
+ }
+ },
+ "prerender": {
+ "builder": "@nguniversal/builders:prerender",
+ "options": {
+ "browserTarget": "programaker:build:production",
+ "serverTarget": "programaker:server:production",
+ "routes": [
+ "/"
+ ]
+ },
+ "configurations": {
+ "production": {}
+ }
}
}
},
- "plaza-e2e": {
+ "programaker-e2e": {
"root": "",
"sourceRoot": "",
"projectType": "application",
@@ -139,7 +232,7 @@
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
- "devServerTarget": "plaza:serve"
+ "devServerTarget": "programaker:serve"
}
},
"lint": {
@@ -154,11 +247,11 @@
}
}
},
- "defaultProject": "plaza",
+ "defaultProject": "programaker",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
- "styleext": "scss"
+ "style": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
diff --git a/frontend/e2e/app.e2e-spec.ts b/frontend/e2e/app.e2e-spec.ts
index 42f977d9..bfd78b6e 100644
--- a/frontend/e2e/app.e2e-spec.ts
+++ b/frontend/e2e/app.e2e-spec.ts
@@ -1,10 +1,10 @@
-import { PlazaPage } from './app.po';
+import { ProgramakerPage } from './app.po';
-describe('plaza App', () => {
- let page: PlazaPage;
+describe('programaker App', () => {
+ let page: ProgramakerPage;
beforeEach(() => {
- page = new PlazaPage();
+ page = new ProgramakerPage();
});
it('should display message saying app works', () => {
diff --git a/frontend/e2e/app.po.ts b/frontend/e2e/app.po.ts
index 0469e7c3..501a82a7 100644
--- a/frontend/e2e/app.po.ts
+++ b/frontend/e2e/app.po.ts
@@ -1,6 +1,6 @@
import { browser, element, by } from 'protractor';
-export class PlazaPage {
+export class ProgramakerPage {
navigateTo() {
return browser.get('/');
}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9a81f4db..bb21405e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,377 +1,441 @@
{
"name": "auto-mate",
"version": "0.0.0",
- "lockfileVersion": 1,
+ "lockfileVersion": 2,
"requires": true,
- "dependencies": {
- "@angular-devkit/architect": {
- "version": "0.900.1",
- "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.900.1.tgz",
- "integrity": "sha512-zzB3J0fXFoYeJpgF5tsmZ7byygzjJn1IPiXBdnbNqcMbil1OPOhq+KdD4ZFPyXNwBQ3w02kOwPdNqB++jbPmlQ==",
+ "packages": {
+ "": {
+ "name": "auto-mate",
+ "version": "0.0.0",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@angular/animations": "^10.0.10",
+ "@angular/cdk": "^10.1.3",
+ "@angular/common": "^10.0.10",
+ "@angular/compiler": "^10.0.10",
+ "@angular/core": "^10.0.10",
+ "@angular/forms": "^10.0.10",
+ "@angular/material": "^10.1.3",
+ "@angular/platform-browser": "^10.0.10",
+ "@angular/platform-browser-dynamic": "^10.0.10",
+ "@angular/platform-server": "^10.0.10",
+ "@angular/router": "^10.0.10",
+ "@ng-toolkit/universal": "^8.1.0",
+ "@nguniversal/express-engine": "^10.0.2",
+ "@ngx-utils/cookies": "https://github.com/kenkeiras/ngx-cookies/releases/download/angular-10-support/ngx-cookies-angular-10.tgz",
+ "@types/cookie-parser": "^1.4.2",
+ "bootstrap": "^4.5.0",
+ "cookie-parser": "^1.4.5",
+ "core-js": "^2.6.11",
+ "express": "^4.15.2",
+ "fuse.js": "^5.2.3",
+ "huebee": "^2.1.0",
+ "jstz": "^2.1.1",
+ "ngx-bootstrap": "^5.6.1",
+ "ngx-toastr": "^13.2.0",
+ "nprogress": "^0.2.0",
+ "rxjs": "^6.5.5",
+ "y-websocket": "^1.3.11",
+ "yjs": "^13.5.0",
+ "zone.js": "^0.10.3"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^0.1000.6",
+ "@angular/cli": "^10.0.6",
+ "@angular/compiler-cli": "^10.0.10",
+ "@nguniversal/builders": "^10.0.2",
+ "@types/express": "^4.17.0",
+ "@types/jasmine": "^3.5.10",
+ "@types/node": "^11.15.12",
+ "codelyzer": "^6.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "google-closure-library": "^20190325.0.0",
+ "jasmine": "^3.5.0",
+ "jasmine-core": "~3.5.0",
+ "jasmine-spec-reporter": "~5.0.0",
+ "jasmine-ts": "^0.3.0",
+ "karma": "~5.0.0",
+ "karma-chrome-launcher": "~3.1.0",
+ "karma-cli": "~2.0.0",
+ "karma-coverage-istanbul-reporter": "~3.0.2",
+ "karma-jasmine": "~3.3.0",
+ "karma-jasmine-html-reporter": "^1.5.0",
+ "node-gyp": "^4.0.0",
+ "nodemon": "^2.0.4",
+ "protractor": "~7.0.0",
+ "scratch-blocks": "0.1.0-prerelease.20200512201140",
+ "tar": "^4.4.13",
+ "ts-node": "^8.10.1",
+ "tslib": "^2.0.0",
+ "tslint": "~6.1.0",
+ "typescript": "~3.9.7"
+ }
+ },
+ "node_modules/@angular-devkit/architect": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1000.6.tgz",
+ "integrity": "sha512-IZ8yiiW+LQ5mI3VbNHzisTIn0j6D1inQZgcZtc5W2A7fFNvBlIh6vGU3mB6Qvg678Gt6tlvnNT6/R9A9Ct7VnA==",
"dev": true,
- "requires": {
- "@angular-devkit/core": "9.0.1",
- "rxjs": "6.5.3"
+ "dependencies": {
+ "@angular-devkit/core": "10.0.6",
+ "rxjs": "6.5.5"
},
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/architect/node_modules/rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
"dependencies": {
- "rxjs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
- "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- }
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
}
},
- "@angular-devkit/build-angular": {
- "version": "0.900.1",
- "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.900.1.tgz",
- "integrity": "sha512-e1/EiNI9UAKJxI9+7KA59A15Rkx2QA86evb9iUuwxWGvIsTsN/sg/oXUZA//nTUQTAht+qWJp3I2amd/nyQZLQ==",
- "dev": true,
- "requires": {
- "@angular-devkit/architect": "0.900.1",
- "@angular-devkit/build-optimizer": "0.900.1",
- "@angular-devkit/build-webpack": "0.900.1",
- "@angular-devkit/core": "9.0.1",
- "@babel/core": "7.7.7",
- "@babel/generator": "7.7.7",
- "@babel/preset-env": "7.7.7",
- "@ngtools/webpack": "9.0.1",
- "ajv": "6.10.2",
- "autoprefixer": "9.7.1",
- "babel-loader": "8.0.6",
- "browserslist": "4.8.3",
- "cacache": "13.0.1",
- "caniuse-lite": "1.0.30001020",
+ "node_modules/@angular-devkit/architect/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/@angular-devkit/build-angular": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.1000.6.tgz",
+ "integrity": "sha512-tKyVD8Wqfo2wFdfWmc7OMzFn30Zl37heEusnMrQD5/zZ3Hw4Nqt2kf3pf3hbWl1GExUVFYyRNoCOCh9DaIfh0w==",
+ "dev": true,
+ "dependencies": {
+ "@angular-devkit/architect": "0.1000.6",
+ "@angular-devkit/build-optimizer": "0.1000.6",
+ "@angular-devkit/build-webpack": "0.1000.6",
+ "@angular-devkit/core": "10.0.6",
+ "@babel/core": "7.9.6",
+ "@babel/generator": "7.9.6",
+ "@babel/plugin-transform-runtime": "7.9.6",
+ "@babel/preset-env": "7.9.6",
+ "@babel/runtime": "7.9.6",
+ "@babel/template": "7.8.6",
+ "@jsdevtools/coverage-istanbul-loader": "3.0.3",
+ "@ngtools/webpack": "10.0.6",
+ "ajv": "6.12.3",
+ "autoprefixer": "9.8.0",
+ "babel-loader": "8.1.0",
+ "browserslist": "^4.9.1",
+ "cacache": "15.0.3",
+ "caniuse-lite": "^1.0.30001032",
"circular-dependency-plugin": "5.2.0",
- "copy-webpack-plugin": "5.1.1",
- "core-js": "3.6.0",
- "coverage-istanbul-loader": "2.0.3",
+ "copy-webpack-plugin": "6.0.3",
+ "core-js": "3.6.4",
+ "css-loader": "3.5.3",
"cssnano": "4.1.10",
- "file-loader": "4.2.0",
- "find-cache-dir": "3.0.0",
- "glob": "7.1.5",
- "jest-worker": "24.9.0",
+ "file-loader": "6.0.0",
+ "find-cache-dir": "3.3.1",
+ "glob": "7.1.6",
+ "jest-worker": "26.0.0",
"karma-source-map-support": "1.4.0",
- "less": "3.10.3",
- "less-loader": "5.0.0",
- "license-webpack-plugin": "2.1.3",
- "loader-utils": "1.2.3",
- "magic-string": "0.25.4",
- "mini-css-extract-plugin": "0.8.0",
+ "less-loader": "6.1.0",
+ "license-webpack-plugin": "2.2.0",
+ "loader-utils": "2.0.0",
+ "mini-css-extract-plugin": "0.9.0",
"minimatch": "3.0.4",
- "open": "7.0.0",
+ "open": "7.0.4",
"parse5": "4.0.0",
- "postcss": "7.0.21",
+ "pnp-webpack-plugin": "1.6.4",
+ "postcss": "7.0.31",
"postcss-import": "12.0.1",
"postcss-loader": "3.0.0",
- "raw-loader": "3.1.0",
- "regenerator-runtime": "0.13.3",
- "rimraf": "3.0.0",
- "rollup": "1.25.2",
- "rxjs": "6.5.3",
- "sass": "1.23.3",
- "sass-loader": "8.0.0",
- "semver": "6.3.0",
+ "raw-loader": "4.0.1",
+ "regenerator-runtime": "0.13.5",
+ "resolve-url-loader": "3.1.1",
+ "rimraf": "3.0.2",
+ "rollup": "2.10.9",
+ "rxjs": "6.5.5",
+ "sass": "1.26.5",
+ "sass-loader": "8.0.2",
+ "semver": "7.3.2",
"source-map": "0.7.3",
- "source-map-loader": "0.2.4",
- "source-map-support": "0.5.16",
- "speed-measure-webpack-plugin": "1.3.1",
- "style-loader": "1.0.0",
+ "source-map-loader": "1.0.0",
+ "source-map-support": "0.5.19",
+ "speed-measure-webpack-plugin": "1.3.3",
+ "style-loader": "1.2.1",
"stylus": "0.54.7",
"stylus-loader": "3.0.2",
- "terser": "4.5.1",
- "terser-webpack-plugin": "2.3.3",
+ "terser": "4.7.0",
+ "terser-webpack-plugin": "3.0.1",
"tree-kill": "1.2.2",
- "webpack": "4.41.2",
+ "webpack": "4.43.0",
"webpack-dev-middleware": "3.7.2",
- "webpack-dev-server": "3.9.0",
+ "webpack-dev-server": "3.11.0",
"webpack-merge": "4.2.2",
"webpack-sources": "1.4.3",
- "webpack-subresource-integrity": "1.3.4",
- "worker-plugin": "3.2.0"
+ "webpack-subresource-integrity": "1.4.1",
+ "worker-plugin": "4.0.3"
},
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/core-js": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
+ "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==",
+ "dev": true
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/parse5": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+ "dev": true
+ },
+ "node_modules/@angular-devkit/build-angular/node_modules/rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
"dependencies": {
- "ajv": {
- "version": "6.10.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
- "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^2.0.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "core-js": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.0.tgz",
- "integrity": "sha512-AHPTNKzyB+YwgDWoSOCaid9PUSEF6781vsfiK8qUz62zRR448/XgK2NtCbpiUGizbep8Lrpt0Du19PpGGZvw3Q==",
- "dev": true
- },
- "glob": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
- "integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "parse5": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
- "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
- "dev": true
- },
- "rimraf": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz",
- "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "rxjs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
- "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- },
- "source-map-support": {
- "version": "0.5.16",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
- "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
- "dev": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- }
- }
- }
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
}
},
- "@angular-devkit/build-optimizer": {
- "version": "0.900.1",
- "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.900.1.tgz",
- "integrity": "sha512-EnIU+ogiJrUPf8+fuPE5xQ+j/qUZDZ/SmLs8XAOmvoOBpZ0vPNedrHBHCxmV+ACbCxHGmIKQ/ZL29XUYVasteg==",
+ "node_modules/@angular-devkit/build-angular/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/@angular-devkit/build-optimizer": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.1000.6.tgz",
+ "integrity": "sha512-R8zDEAvd9PeUKvOKh6I7xp3w+MViCwjGKoOZcznjH/i/9PQjOHCMwU5S48RQloQjMGu96eDMUGOVnd9qkzXUEw==",
"dev": true,
- "requires": {
- "loader-utils": "1.2.3",
+ "dependencies": {
+ "loader-utils": "2.0.0",
"source-map": "0.7.3",
- "tslib": "1.10.0",
- "typescript": "3.6.4",
+ "tslib": "2.0.0",
"webpack-sources": "1.4.3"
},
- "dependencies": {
- "typescript": {
- "version": "3.6.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
- "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
- "dev": true
- }
+ "bin": {
+ "build-optimizer": "src/build-optimizer/cli.js"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
}
},
- "@angular-devkit/build-webpack": {
- "version": "0.900.1",
- "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.900.1.tgz",
- "integrity": "sha512-GwV+jht42S2XZZbvy07mXqZ5us9ppbIi/gCL5SiUh+xtSdZGbfE6RoFZXmeOuxBn9FY0vUMTFtKCK5Mx8O3WYg==",
+ "node_modules/@angular-devkit/build-optimizer/node_modules/tslib": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
+ "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==",
+ "dev": true
+ },
+ "node_modules/@angular-devkit/build-webpack": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1000.6.tgz",
+ "integrity": "sha512-R01bJWuvckU5IdjcqoCeikLBpHRqt5fgfD0a4Hsg3evqW6xxXcSgc+YhWfeEmyU/nF/kVel8G2bFyPzhZP4QdQ==",
"dev": true,
- "requires": {
- "@angular-devkit/architect": "0.900.1",
- "@angular-devkit/core": "9.0.1",
- "rxjs": "6.5.3"
+ "dependencies": {
+ "@angular-devkit/architect": "0.1000.6",
+ "@angular-devkit/core": "10.0.6",
+ "rxjs": "6.5.5"
},
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
"dependencies": {
- "rxjs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
- "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- }
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
}
},
- "@angular-devkit/core": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.0.1.tgz",
- "integrity": "sha512-HboJI/x+SJD9clSOAMjHRv0eXAGRAdEaqJGmjDfdFMP2wznfsBiC6cgcHC17oM4jRWFhmWMR8Omc7CjLZJawJg==",
+ "node_modules/@angular-devkit/build-webpack/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/@angular-devkit/core": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.0.6.tgz",
+ "integrity": "sha512-mVvqSEoeErZ7bAModk95EAa6R9Nl23rvX+/TXuKVTK2dziMFBOrwHjb1DYhnZxFIH4xfUftCx+BWHjXBXCPYlA==",
"dev": true,
- "requires": {
- "ajv": "6.10.2",
- "fast-json-stable-stringify": "2.0.0",
- "magic-string": "0.25.4",
- "rxjs": "6.5.3",
+ "dependencies": {
+ "ajv": "6.12.3",
+ "fast-json-stable-stringify": "2.1.0",
+ "magic-string": "0.25.7",
+ "rxjs": "6.5.5",
"source-map": "0.7.3"
},
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/core/node_modules/rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
"dependencies": {
- "ajv": {
- "version": "6.10.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
- "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^2.0.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "rxjs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
- "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- }
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
}
},
- "@angular-devkit/schematics": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-9.0.1.tgz",
- "integrity": "sha512-Cuub9eJm1TWygKTOowRbxMASA8QWeHWzNEU2V3TqUF1Tqy/iPf4cpuMijkFysXjTn2bi2HA9t26AwQkwymbliA==",
+ "node_modules/@angular-devkit/core/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/@angular-devkit/schematics": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-10.0.6.tgz",
+ "integrity": "sha512-V3T4cf+jVKiPYyBrSVHf3ZSnk4wIc1WEaaeFta56HccEGQCQpvAFKqDurmtMHer50Hhaxhn7IC3Oi5kPnvkNyQ==",
"dev": true,
- "requires": {
- "@angular-devkit/core": "9.0.1",
- "ora": "4.0.2",
- "rxjs": "6.5.3"
+ "dependencies": {
+ "@angular-devkit/core": "10.0.6",
+ "ora": "4.0.4",
+ "rxjs": "6.5.5"
},
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@angular-devkit/schematics/node_modules/rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
"dependencies": {
- "rxjs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
- "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- }
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
}
},
- "@angular/animations": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.0.0.tgz",
- "integrity": "sha512-jB8+SC3vMztW5zt5UYVmtVwqIWE33UyEjbP5JPba3I3bLRK5E059LcJmN1rSdJHItgIAdG9Y1I0WJ6aiSFyp4Q=="
+ "node_modules/@angular-devkit/schematics/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
},
- "@angular/cdk": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.0.0.tgz",
- "integrity": "sha512-2kYpyYbewIB6fubSIDMvSprJLNplRZoL/AtXW3od4dLyRxtzX+7iWTAtzUG/dhq8CKev0lpd1HENh5lLR/Lhjw==",
- "requires": {
- "parse5": "^5.0.0"
+ "node_modules/@angular/animations": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-10.0.10.tgz",
+ "integrity": "sha512-lIbNeLVVl9bO41orPFpKoobCvxZIZ2wdcKJBEFtQiOdw0khRQQ8k7so4TAWOZXRJR+MkOUCjU2pO8gbMXgBweQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
}
},
- "@angular/cli": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-9.0.1.tgz",
- "integrity": "sha512-/nykTIqZq1plxaXVoMzAqjnExGhkYoSoq88AE4Mb31d6n/SW2DFh62C3hze+atI6YLqeFaPhYuA5zG+z3oOXbQ==",
+ "node_modules/@angular/cdk": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-10.1.3.tgz",
+ "integrity": "sha512-xMV1M41mfuaQod4rtAG/duYiWffGIC2C87E1YuyHTh8SEcHopGVRQd2C8PWH+iwinPbes7AjU1uzCEvmOYikrA==",
+ "dependencies": {
+ "parse5": "^5.0.0",
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@angular/cli": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-10.0.6.tgz",
+ "integrity": "sha512-gQbQA/CsCyMf9RKEv1hJBCdBebV2BHeT4lGi56Eii0IkvZD5WIH0dNfQzR+6ErqGDgE1EI+9YCuX3psMEvCRUA==",
"dev": true,
- "requires": {
- "@angular-devkit/architect": "0.900.1",
- "@angular-devkit/core": "9.0.1",
- "@angular-devkit/schematics": "9.0.1",
- "@schematics/angular": "9.0.1",
- "@schematics/update": "0.900.1",
+ "dependencies": {
+ "@angular-devkit/architect": "0.1000.6",
+ "@angular-devkit/core": "10.0.6",
+ "@angular-devkit/schematics": "10.0.6",
+ "@schematics/angular": "10.0.6",
+ "@schematics/update": "0.1000.6",
"@yarnpkg/lockfile": "1.1.0",
"ansi-colors": "4.1.1",
- "debug": "^4.1.1",
+ "debug": "4.1.1",
"ini": "1.3.5",
- "inquirer": "7.0.0",
- "npm-package-arg": "6.1.1",
- "npm-pick-manifest": "3.0.2",
- "open": "7.0.0",
- "pacote": "9.5.8",
+ "inquirer": "7.1.0",
+ "npm-package-arg": "8.0.1",
+ "npm-pick-manifest": "6.1.0",
+ "open": "7.0.4",
+ "pacote": "9.5.12",
"read-package-tree": "5.3.1",
- "rimraf": "3.0.0",
- "semver": "6.3.0",
+ "rimraf": "3.0.2",
+ "semver": "7.3.2",
"symbol-observable": "1.2.0",
- "universal-analytics": "^0.4.20",
- "uuid": "^3.3.2"
+ "universal-analytics": "0.4.20",
+ "uuid": "8.1.0"
},
- "dependencies": {
- "ansi-colors": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
- "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
- "dev": true
- },
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
- "rimraf": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz",
- "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- }
+ "bin": {
+ "ng": "bin/ng"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
}
},
- "@angular/common": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/common/-/common-9.0.0.tgz",
- "integrity": "sha512-ZMmEClGtUNJwV5CBlqcSHPIsNyz6WU/GvKWFzJ5VZc68oeg1e7lqfNMNIC47TjyolNJ7VSpNlyrKjzfdBlmqVw=="
+ "node_modules/@angular/cli/node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
},
- "@angular/compiler": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz",
- "integrity": "sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ=="
+ "node_modules/@angular/cli/node_modules/uuid": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
+ "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==",
+ "dev": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
},
- "@angular/compiler-cli": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-9.0.0.tgz",
- "integrity": "sha512-6L3swd3Z2ceAapmioml6z7yu3bYC2aVm3/rgK7eCoZtPcevuvTpGnXcFSVvNgByV51GntgInThPbMx0xY23Rvw==",
+ "node_modules/@angular/common": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/common/-/common-10.0.10.tgz",
+ "integrity": "sha512-p6/pTk0s0Ai5uUkOHHFZwp+TjxRNPldPxTU2LVxg2xuBEQTO53BsfBKn3zi74epdb1kBC0Yjdj6yEL4dITBs7A==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@angular/compiler": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-10.0.10.tgz",
+ "integrity": "sha512-fO7kml0HUgnMa5eviKUk+j7NACASkoMAEgvbcVdKmGsSDu9YVkaqSdLXuj2vu9glSJWDRkZJKSrt9MzbmhyB5A==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@angular/compiler-cli": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-10.0.10.tgz",
+ "integrity": "sha512-XkvWdJKr6HkyzAbcmy99HyDR4z949z9nHGwHNLBQjLbkX11i03fvS3bI5kgwqtNiLWYqxiPfXnpAyLBeFghCcw==",
"dev": true,
- "requires": {
+ "dependencies": {
"canonical-path": "1.0.0",
"chokidar": "^3.0.0",
"convert-source-map": "^1.5.1",
@@ -382,5519 +446,25499 @@
"reflect-metadata": "^0.1.2",
"semver": "^6.3.0",
"source-map": "^0.6.1",
- "yargs": "13.1.0"
+ "sourcemap-codec": "^1.4.8",
+ "tslib": "^2.0.0",
+ "yargs": "15.3.0"
},
+ "bin": {
+ "ivy-ngcc": "ngcc/main-ivy-ngcc.js",
+ "ng-xi18n": "src/extract_i18n.js",
+ "ngc": "src/main.js",
+ "ngcc": "ngcc/main-ngcc.js"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
"dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
- "dev": true
- },
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
- "dev": true
- },
- "fs-extra": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz",
- "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
- }
- },
- "get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
- "dev": true
- },
- "require-main-filename": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
- "dev": true
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
- "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- }
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
- "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- },
- "yargs": {
- "version": "13.1.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.1.0.tgz",
- "integrity": "sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg==",
- "dev": true,
- "requires": {
- "cliui": "^4.0.0",
- "find-up": "^3.0.0",
- "get-caller-file": "^2.0.1",
- "os-locale": "^3.1.0",
- "require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
- "string-width": "^3.0.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^13.0.0"
- }
- },
- "yargs-parser": {
- "version": "13.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
- "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
- "dev": true,
- "requires": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- }
- }
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "@angular/core": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz",
- "integrity": "sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w=="
- },
- "@angular/forms": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-9.0.0.tgz",
- "integrity": "sha512-SIYJc0Rgaihow1t+iiwSFGEvvRgssgUuxwIYbMfCp1Sx513K+JX9nVFXqU+dcGj/eF1u5wwYwbvlVyuMQLzmXg=="
+ "node_modules/@angular/compiler-cli/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
},
- "@angular/material": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/material/-/material-9.0.0.tgz",
- "integrity": "sha512-QxN2rmR5mvg2YE1NoIGWLpbnmcJq0iFidzy6odzvN17+XkoCJBZ65IdYsHrJgfwGpoIy6bywuixrDHHcSh9I5w=="
+ "node_modules/@angular/compiler-cli/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
},
- "@angular/platform-browser": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.0.0.tgz",
- "integrity": "sha512-2PR/o57HjZvKEnAF8ODeqxmeC90oth9dLTMrJNoI5MET0IeErKeI/9Sl5cLQuXC+lSVN5rOMCvDb74VWSno5yw=="
+ "node_modules/@angular/compiler-cli/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
- "@angular/platform-browser-dynamic": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.0.0.tgz",
- "integrity": "sha512-F1kbEpmDottTemRPEOAz2Te5ABVJ7wypfzBllxqXbdxPHvYLfL8db2dXyiGqABQ3ZFHPLNilrkUTy0sbuuU4OA=="
+ "node_modules/@angular/compiler-cli/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
},
- "@angular/router": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/@angular/router/-/router-9.0.0.tgz",
- "integrity": "sha512-yyOcStpgN5t8wGRNO85mo0jplXkntP+v2tmSxNx45pahqmofSFm+QCEFa2zHQuMr7NoiGERhd0Tae7NDCCjtjA=="
+ "node_modules/@angular/compiler-cli/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
- "@babel/code-frame": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
- "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+ "node_modules/@angular/compiler-cli/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
- "requires": {
- "@babel/highlight": "^7.0.0"
+ "engines": {
+ "node": ">=8"
}
},
- "@babel/core": {
- "version": "7.7.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz",
- "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.5.5",
- "@babel/generator": "^7.7.7",
- "@babel/helpers": "^7.7.4",
- "@babel/parser": "^7.7.7",
- "@babel/template": "^7.7.4",
- "@babel/traverse": "^7.7.4",
- "@babel/types": "^7.7.4",
- "convert-source-map": "^1.7.0",
- "debug": "^4.1.0",
- "json5": "^2.1.0",
- "lodash": "^4.17.13",
- "resolve": "^1.3.2",
- "semver": "^5.4.1",
- "source-map": "^0.5.0"
- },
+ "node_modules/@angular/compiler-cli/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- },
- "json5": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz",
- "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==",
- "dev": true,
- "requires": {
- "minimist": "^1.2.0"
- }
- },
- "minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
- "dev": true
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- }
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "@babel/generator": {
- "version": "7.7.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz",
- "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==",
+ "node_modules/@angular/compiler-cli/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
- "requires": {
- "@babel/types": "^7.7.4",
- "jsesc": "^2.5.1",
- "lodash": "^4.17.13",
- "source-map": "^0.5.0"
- },
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- }
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "@babel/helper-annotate-as-pure": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz",
- "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==",
+ "node_modules/@angular/compiler-cli/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- },
- "dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "engines": {
+ "node": ">=8"
}
},
- "@babel/helper-builder-binary-assignment-operator-visitor": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz",
- "integrity": "sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==",
+ "node_modules/@angular/compiler-cli/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
- "requires": {
- "@babel/helper-explode-assignable-expression": "^7.8.3",
- "@babel/types": "^7.8.3"
- },
- "dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "bin": {
+ "semver": "bin/semver.js"
}
},
- "@babel/helper-call-delegate": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.8.3.tgz",
- "integrity": "sha512-6Q05px0Eb+N4/GTyKPPvnkig7Lylw+QzihMpws9iiZQv7ZImf84ZsZpQH7QoWN4n4tm81SnSzPgHw2qtO0Zf3A==",
+ "node_modules/@angular/compiler-cli/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
- "requires": {
- "@babel/helper-hoist-variables": "^7.8.3",
- "@babel/traverse": "^7.8.3",
- "@babel/types": "^7.8.3"
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
},
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "@babel/helper-create-regexp-features-plugin": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz",
- "integrity": "sha512-Gcsm1OHCUr9o9TcJln57xhWHtdXbA2pgQ58S0Lxlks0WMGNXuki4+GLfX0p+L2ZkINUGZvfkz8rzoqJQSthI+Q==",
+ "node_modules/@angular/compiler-cli/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
- "requires": {
- "@babel/helper-regex": "^7.8.3",
- "regexpu-core": "^4.6.0"
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
},
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@angular/compiler-cli/node_modules/yargs": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz",
+ "integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==",
+ "dev": true,
"dependencies": {
- "jsesc": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
- "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
- "dev": true
- },
- "regexpu-core": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz",
- "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==",
- "dev": true,
- "requires": {
- "regenerate": "^1.4.0",
- "regenerate-unicode-properties": "^8.1.0",
- "regjsgen": "^0.5.0",
- "regjsparser": "^0.6.0",
- "unicode-match-property-ecmascript": "^1.0.4",
- "unicode-match-property-value-ecmascript": "^1.1.0"
- }
- },
- "regjsgen": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz",
- "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==",
- "dev": true
- },
- "regjsparser": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.2.tgz",
- "integrity": "sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==",
- "dev": true,
- "requires": {
- "jsesc": "~0.5.0"
- }
- }
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "@babel/helper-define-map": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz",
- "integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==",
+ "node_modules/@angular/compiler-cli/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
- "requires": {
- "@babel/helper-function-name": "^7.8.3",
- "@babel/types": "^7.8.3",
- "lodash": "^4.17.13"
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
},
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@angular/core": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-10.0.10.tgz",
+ "integrity": "sha512-PIQhLqjZayVXJoXs4WQu7orkePqFiux19y7bgBrsSAithe+g9BkrSIdX7+tkkX0zggUWKywY92YuMZCJ/S+uiw==",
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/helper-function-name": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
- "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
- "dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/helper-get-function-arity": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
- "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "tslib": "^2.0.0"
}
},
- "@babel/helper-explode-assignable-expression": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz",
- "integrity": "sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==",
- "dev": true,
- "requires": {
- "@babel/traverse": "^7.8.3",
- "@babel/types": "^7.8.3"
+ "node_modules/@angular/forms": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-10.0.10.tgz",
+ "integrity": "sha512-bWjbsqMTiCNQZzXAfiEwT/tiAzSvChnqBimrJWNSHVYRkp71TkDcKXn6mA+E//YR0eZ84GKNNiVlKFxqkmeyqQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@angular/material": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/@angular/material/-/material-10.1.3.tgz",
+ "integrity": "sha512-6ygbCVcejFydmZUlOcNreiWQTvL4kOrEp/M51DV70hqffTnxajCzaRe2MQhxisENB/bR8mtMvf8YY3Rsys/HCw==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@angular/platform-browser": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-10.0.10.tgz",
+ "integrity": "sha512-srNGkvg9177skff7QOe3L+nGOSbrKLzFt3Z5O3oM0N0TWr8QlWEA+zQm8n0zLHI8AmdZbmFzAYYJiBvVCSc5RQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@angular/platform-browser-dynamic": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-10.0.10.tgz",
+ "integrity": "sha512-6jbn0Ldyc+80BCETGtE7pzfKlbjfa/wEPhLEGWoYtxrrJ5UB3CblGpDMOsv1ibOQijPZ/JSmIMmAxz66+pLx3g==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "node_modules/@angular/platform-server": {
+ "version": "10.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-10.1.5.tgz",
+ "integrity": "sha512-n+6LEklqyzVdMiHRoGTU1MXECL/f6PdrLOJ8p5w5vak8dLQu83AHTO8SNC/YjrLanLgEXZXTG76AfGJbcMbiEw==",
+ "dependencies": {
+ "domino": "^2.1.2",
+ "tslib": "^2.0.0",
+ "xhr2": "^0.2.0"
},
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/@angular/router": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/router/-/router-10.0.10.tgz",
+ "integrity": "sha512-wDmr/Spuv4OhPK5a49AvgJhaedRw4yb7nmPMd51sWqzOV31RRcGXORjiXZOcSpElLxM9f7JV0tWDR5p5ko/kPA==",
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "tslib": "^2.0.0"
}
},
- "@babel/helper-function-name": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
- "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
+ "node_modules/@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
"dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.0.0",
- "@babel/template": "^7.1.0",
- "@babel/types": "^7.0.0"
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
}
},
- "@babel/helper-get-function-arity": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz",
- "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
+ "node_modules/@babel/compat-data": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz",
+ "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==",
"dev": true,
- "requires": {
- "@babel/types": "^7.0.0"
+ "dependencies": {
+ "browserslist": "^4.12.0",
+ "invariant": "^2.2.4",
+ "semver": "^5.5.0"
}
},
- "@babel/helper-hoist-variables": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz",
- "integrity": "sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==",
+ "node_modules/@babel/compat-data/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- },
- "dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "bin": {
+ "semver": "bin/semver"
}
},
- "@babel/helper-member-expression-to-functions": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz",
- "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==",
+ "node_modules/@babel/core": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz",
+ "integrity": "sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg==",
"dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
+ "dependencies": {
+ "@babel/code-frame": "^7.8.3",
+ "@babel/generator": "^7.9.6",
+ "@babel/helper-module-transforms": "^7.9.0",
+ "@babel/helpers": "^7.9.6",
+ "@babel/parser": "^7.9.6",
+ "@babel/template": "^7.8.6",
+ "@babel/traverse": "^7.9.6",
+ "@babel/types": "^7.9.6",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.1",
+ "json5": "^2.1.2",
+ "lodash": "^4.17.13",
+ "resolve": "^1.3.2",
+ "semver": "^5.4.1",
+ "source-map": "^0.5.0"
},
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/@babel/core/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz",
+ "integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==",
+ "dev": true,
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "@babel/types": "^7.9.6",
+ "jsesc": "^2.5.1",
+ "lodash": "^4.17.13",
+ "source-map": "^0.5.0"
}
},
- "@babel/helper-module-imports": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz",
- "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==",
+ "node_modules/@babel/generator/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz",
+ "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==",
"dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "@babel/types": "^7.10.4"
}
},
- "@babel/helper-module-transforms": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz",
- "integrity": "sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q==",
+ "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz",
+ "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==",
"dev": true,
- "requires": {
- "@babel/helper-module-imports": "^7.8.3",
- "@babel/helper-simple-access": "^7.8.3",
- "@babel/helper-split-export-declaration": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/types": "^7.8.3",
- "lodash": "^4.17.13"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/helper-explode-assignable-expression": "^7.10.4",
+ "@babel/types": "^7.10.4"
}
},
- "@babel/helper-optimise-call-expression": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz",
- "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==",
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz",
+ "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==",
"dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "@babel/compat-data": "^7.10.4",
+ "browserslist": "^4.12.0",
+ "invariant": "^2.2.4",
+ "levenary": "^1.1.1",
+ "semver": "^5.5.0"
}
},
- "@babel/helper-plugin-utils": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
- "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==",
- "dev": true
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
},
- "@babel/helper-regex": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz",
- "integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==",
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz",
+ "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==",
"dev": true,
- "requires": {
- "lodash": "^4.17.13"
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-regex": "^7.10.4",
+ "regexpu-core": "^4.7.0"
}
},
- "@babel/helper-remap-async-to-generator": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz",
- "integrity": "sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==",
+ "node_modules/@babel/helper-define-map": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz",
+ "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==",
"dev": true,
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.8.3",
- "@babel/helper-wrap-function": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/traverse": "^7.8.3",
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/types": "^7.10.5",
+ "lodash": "^4.17.19"
}
},
- "@babel/helper-replace-supers": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz",
- "integrity": "sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==",
+ "node_modules/@babel/helper-explode-assignable-expression": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz",
+ "integrity": "sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A==",
"dev": true,
- "requires": {
- "@babel/helper-member-expression-to-functions": "^7.8.3",
- "@babel/helper-optimise-call-expression": "^7.8.3",
- "@babel/traverse": "^7.8.3",
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
}
},
- "@babel/helper-simple-access": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz",
- "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==",
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz",
+ "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==",
"dev": true,
- "requires": {
- "@babel/template": "^7.8.3",
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/helper-get-function-arity": "^7.10.4",
+ "@babel/template": "^7.10.4",
+ "@babel/types": "^7.10.4"
}
},
- "@babel/helper-split-export-declaration": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz",
- "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==",
+ "node_modules/@babel/helper-function-name/node_modules/@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
"dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
}
},
- "@babel/helper-wrap-function": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz",
- "integrity": "sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==",
+ "node_modules/@babel/helper-get-function-arity": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz",
+ "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==",
"dev": true,
- "requires": {
- "@babel/helper-function-name": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/traverse": "^7.8.3",
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/helper-function-name": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
- "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
- "dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/helper-get-function-arity": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
- "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/types": "^7.10.4"
}
},
- "@babel/helpers": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz",
- "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==",
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz",
+ "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==",
"dev": true,
- "requires": {
- "@babel/template": "^7.8.3",
- "@babel/traverse": "^7.8.4",
- "@babel/types": "^7.8.3"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/types": "^7.10.4"
}
},
- "@babel/highlight": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
- "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz",
+ "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==",
"dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- },
"dependencies": {
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/types": "^7.11.0"
}
},
- "@babel/parser": {
- "version": "7.3.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.4.tgz",
- "integrity": "sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==",
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz",
+ "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz",
+ "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.10.4",
+ "@babel/helper-replace-supers": "^7.10.4",
+ "@babel/helper-simple-access": "^7.10.4",
+ "@babel/helper-split-export-declaration": "^7.11.0",
+ "@babel/template": "^7.10.4",
+ "@babel/types": "^7.11.0",
+ "lodash": "^4.17.19"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms/node_modules/@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz",
+ "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
+ "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"dev": true
},
- "@babel/plugin-proposal-async-generator-functions": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz",
- "integrity": "sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==",
+ "node_modules/@babel/helper-regex": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz",
+ "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
- "@babel/helper-remap-async-to-generator": "^7.8.3",
+ "dependencies": {
+ "lodash": "^4.17.19"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz",
+ "integrity": "sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-wrap-function": "^7.10.4",
+ "@babel/template": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz",
+ "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.10.4",
+ "@babel/helper-optimise-call-expression": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz",
+ "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-simple-access/node_modules/@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz",
+ "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.11.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz",
+ "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.11.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
+ "dev": true
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz",
+ "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/template": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz",
+ "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helpers/node_modules/@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.11.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz",
+ "integrity": "sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==",
+ "dev": true,
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-async-generator-functions": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz",
+ "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-remap-async-to-generator": "^7.10.4",
"@babel/plugin-syntax-async-generators": "^7.8.0"
}
},
- "@babel/plugin-proposal-dynamic-import": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz",
- "integrity": "sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==",
+ "node_modules/@babel/plugin-proposal-dynamic-import": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz",
+ "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-dynamic-import": "^7.8.0"
}
},
- "@babel/plugin-proposal-json-strings": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz",
- "integrity": "sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==",
+ "node_modules/@babel/plugin-proposal-json-strings": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz",
+ "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-json-strings": "^7.8.0"
}
},
- "@babel/plugin-proposal-object-rest-spread": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz",
- "integrity": "sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA==",
+ "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz",
+ "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
- "@babel/plugin-syntax-object-rest-spread": "^7.8.0"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0"
}
},
- "@babel/plugin-proposal-optional-catch-binding": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz",
- "integrity": "sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==",
+ "node_modules/@babel/plugin-proposal-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-object-rest-spread": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz",
+ "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
+ "@babel/plugin-transform-parameters": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-optional-catch-binding": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz",
+ "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.0"
}
},
- "@babel/plugin-proposal-unicode-property-regex": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz",
- "integrity": "sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ==",
+ "node_modules/@babel/plugin-proposal-optional-chaining": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz",
+ "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==",
"dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.0"
}
},
- "@babel/plugin-syntax-async-generators": {
+ "node_modules/@babel/plugin-proposal-unicode-property-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz",
+ "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
"integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
"dev": true,
- "requires": {
+ "dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
}
},
- "@babel/plugin-syntax-dynamic-import": {
+ "node_modules/@babel/plugin-syntax-dynamic-import": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
"integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
"dev": true,
- "requires": {
+ "dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
}
},
- "@babel/plugin-syntax-json-strings": {
+ "node_modules/@babel/plugin-syntax-json-strings": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
"integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
"dev": true,
- "requires": {
+ "dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
}
},
- "@babel/plugin-syntax-object-rest-spread": {
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
"integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
"dev": true,
- "requires": {
+ "dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
}
},
- "@babel/plugin-syntax-optional-catch-binding": {
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
"integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
"dev": true,
- "requires": {
+ "dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
}
},
- "@babel/plugin-syntax-top-level-await": {
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
"version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz",
- "integrity": "sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
}
},
- "@babel/plugin-transform-arrow-functions": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz",
- "integrity": "sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==",
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz",
+ "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-async-to-generator": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz",
- "integrity": "sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==",
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz",
+ "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==",
"dev": true,
- "requires": {
- "@babel/helper-module-imports": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3",
- "@babel/helper-remap-async-to-generator": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-block-scoped-functions": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz",
- "integrity": "sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==",
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz",
+ "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-remap-async-to-generator": "^7.10.4"
}
},
- "@babel/plugin-transform-block-scoping": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz",
- "integrity": "sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==",
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz",
+ "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
- "lodash": "^4.17.13"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-classes": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz",
- "integrity": "sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w==",
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.11.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz",
+ "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==",
"dev": true,
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.8.3",
- "@babel/helper-define-map": "^7.8.3",
- "@babel/helper-function-name": "^7.8.3",
- "@babel/helper-optimise-call-expression": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3",
- "@babel/helper-replace-supers": "^7.8.3",
- "@babel/helper-split-export-declaration": "^7.8.3",
- "globals": "^11.1.0"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/helper-function-name": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
- "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
- "dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/helper-get-function-arity": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
- "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-computed-properties": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz",
- "integrity": "sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==",
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz",
+ "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-define-map": "^7.10.4",
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/helper-optimise-call-expression": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-replace-supers": "^7.10.4",
+ "@babel/helper-split-export-declaration": "^7.10.4",
+ "globals": "^11.1.0"
}
},
- "@babel/plugin-transform-destructuring": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz",
- "integrity": "sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ==",
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz",
+ "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-dotall-regex": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz",
- "integrity": "sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==",
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz",
+ "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==",
"dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-duplicate-keys": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz",
- "integrity": "sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==",
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz",
+ "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-exponentiation-operator": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz",
- "integrity": "sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==",
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz",
+ "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==",
"dev": true,
- "requires": {
- "@babel/helper-builder-binary-assignment-operator-visitor": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-for-of": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz",
- "integrity": "sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A==",
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz",
+ "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-function-name": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz",
- "integrity": "sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==",
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz",
+ "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==",
"dev": true,
- "requires": {
- "@babel/helper-function-name": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/helper-function-name": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
- "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
- "dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/helper-get-function-arity": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
- "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- }
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-literals": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz",
- "integrity": "sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==",
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz",
+ "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-member-expression-literals": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz",
- "integrity": "sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==",
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz",
+ "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-modules-amd": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz",
- "integrity": "sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ==",
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz",
+ "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==",
"dev": true,
- "requires": {
- "@babel/helper-module-transforms": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3",
- "babel-plugin-dynamic-import-node": "^2.3.0"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-modules-commonjs": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz",
- "integrity": "sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg==",
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz",
+ "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==",
"dev": true,
- "requires": {
- "@babel/helper-module-transforms": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3",
- "@babel/helper-simple-access": "^7.8.3",
- "babel-plugin-dynamic-import-node": "^2.3.0"
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.10.5",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "babel-plugin-dynamic-import-node": "^2.3.3"
}
},
- "@babel/plugin-transform-modules-systemjs": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz",
- "integrity": "sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg==",
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz",
+ "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==",
"dev": true,
- "requires": {
- "@babel/helper-hoist-variables": "^7.8.3",
- "@babel/helper-module-transforms": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3",
- "babel-plugin-dynamic-import-node": "^2.3.0"
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-simple-access": "^7.10.4",
+ "babel-plugin-dynamic-import-node": "^2.3.3"
}
},
- "@babel/plugin-transform-modules-umd": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz",
- "integrity": "sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw==",
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz",
+ "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==",
"dev": true,
- "requires": {
- "@babel/helper-module-transforms": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-hoist-variables": "^7.10.4",
+ "@babel/helper-module-transforms": "^7.10.5",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "babel-plugin-dynamic-import-node": "^2.3.3"
}
},
- "@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz",
- "integrity": "sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==",
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz",
+ "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==",
"dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-new-target": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz",
- "integrity": "sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==",
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz",
+ "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4"
}
},
- "@babel/plugin-transform-object-super": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz",
- "integrity": "sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==",
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz",
+ "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
- "@babel/helper-replace-supers": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-parameters": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.4.tgz",
- "integrity": "sha512-IsS3oTxeTsZlE5KqzTbcC2sV0P9pXdec53SU+Yxv7o/6dvGM5AkTotQKhoSffhNgZ/dftsSiOoxy7evCYJXzVA==",
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz",
+ "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==",
"dev": true,
- "requires": {
- "@babel/helper-call-delegate": "^7.8.3",
- "@babel/helper-get-function-arity": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
- },
"dependencies": {
- "@babel/helper-get-function-arity": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
- "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-replace-supers": "^7.10.4"
}
},
- "@babel/plugin-transform-property-literals": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz",
- "integrity": "sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==",
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz",
+ "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-get-function-arity": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-regenerator": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz",
- "integrity": "sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA==",
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz",
+ "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==",
"dev": true,
- "requires": {
- "regenerator-transform": "^0.14.0"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-reserved-words": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz",
- "integrity": "sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==",
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz",
+ "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "regenerator-transform": "^0.14.2"
}
},
- "@babel/plugin-transform-shorthand-properties": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz",
- "integrity": "sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==",
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz",
+ "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-spread": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz",
- "integrity": "sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==",
+ "node_modules/@babel/plugin-transform-runtime": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.6.tgz",
+ "integrity": "sha512-qcmiECD0mYOjOIt8YHNsAP1SxPooC/rDmfmiSK9BNY72EitdSc7l44WTEklaWuFtbOEBjNhWWyph/kOImbNJ4w==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.8.3",
+ "@babel/helper-plugin-utils": "^7.8.3",
+ "resolve": "^1.8.1",
+ "semver": "^5.5.1"
}
},
- "@babel/plugin-transform-sticky-regex": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz",
- "integrity": "sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==",
+ "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3",
- "@babel/helper-regex": "^7.8.3"
+ "bin": {
+ "semver": "bin/semver"
}
},
- "@babel/plugin-transform-template-literals": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz",
- "integrity": "sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==",
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz",
+ "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==",
"dev": true,
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/plugin-transform-typeof-symbol": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz",
- "integrity": "sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==",
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz",
+ "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==",
"dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0"
}
},
- "@babel/plugin-transform-unicode-regex": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz",
- "integrity": "sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==",
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz",
+ "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==",
"dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.8.3",
- "@babel/helper-plugin-utils": "^7.8.3"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-regex": "^7.10.4"
}
},
- "@babel/preset-env": {
- "version": "7.7.7",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.7.7.tgz",
- "integrity": "sha512-pCu0hrSSDVI7kCVUOdcMNQEbOPJ52E+LrQ14sN8uL2ALfSqePZQlKrOy+tM4uhEdYlCHi4imr8Zz2cZe9oSdIg==",
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz",
+ "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==",
"dev": true,
- "requires": {
- "@babel/helper-module-imports": "^7.7.4",
- "@babel/helper-plugin-utils": "^7.0.0",
- "@babel/plugin-proposal-async-generator-functions": "^7.7.4",
- "@babel/plugin-proposal-dynamic-import": "^7.7.4",
- "@babel/plugin-proposal-json-strings": "^7.7.4",
- "@babel/plugin-proposal-object-rest-spread": "^7.7.7",
- "@babel/plugin-proposal-optional-catch-binding": "^7.7.4",
- "@babel/plugin-proposal-unicode-property-regex": "^7.7.7",
- "@babel/plugin-syntax-async-generators": "^7.7.4",
- "@babel/plugin-syntax-dynamic-import": "^7.7.4",
- "@babel/plugin-syntax-json-strings": "^7.7.4",
- "@babel/plugin-syntax-object-rest-spread": "^7.7.4",
- "@babel/plugin-syntax-optional-catch-binding": "^7.7.4",
- "@babel/plugin-syntax-top-level-await": "^7.7.4",
- "@babel/plugin-transform-arrow-functions": "^7.7.4",
- "@babel/plugin-transform-async-to-generator": "^7.7.4",
- "@babel/plugin-transform-block-scoped-functions": "^7.7.4",
- "@babel/plugin-transform-block-scoping": "^7.7.4",
- "@babel/plugin-transform-classes": "^7.7.4",
- "@babel/plugin-transform-computed-properties": "^7.7.4",
- "@babel/plugin-transform-destructuring": "^7.7.4",
- "@babel/plugin-transform-dotall-regex": "^7.7.7",
- "@babel/plugin-transform-duplicate-keys": "^7.7.4",
- "@babel/plugin-transform-exponentiation-operator": "^7.7.4",
- "@babel/plugin-transform-for-of": "^7.7.4",
- "@babel/plugin-transform-function-name": "^7.7.4",
- "@babel/plugin-transform-literals": "^7.7.4",
- "@babel/plugin-transform-member-expression-literals": "^7.7.4",
- "@babel/plugin-transform-modules-amd": "^7.7.5",
- "@babel/plugin-transform-modules-commonjs": "^7.7.5",
- "@babel/plugin-transform-modules-systemjs": "^7.7.4",
- "@babel/plugin-transform-modules-umd": "^7.7.4",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.7.4",
- "@babel/plugin-transform-new-target": "^7.7.4",
- "@babel/plugin-transform-object-super": "^7.7.4",
- "@babel/plugin-transform-parameters": "^7.7.7",
- "@babel/plugin-transform-property-literals": "^7.7.4",
- "@babel/plugin-transform-regenerator": "^7.7.5",
- "@babel/plugin-transform-reserved-words": "^7.7.4",
- "@babel/plugin-transform-shorthand-properties": "^7.7.4",
- "@babel/plugin-transform-spread": "^7.7.4",
- "@babel/plugin-transform-sticky-regex": "^7.7.4",
- "@babel/plugin-transform-template-literals": "^7.7.4",
- "@babel/plugin-transform-typeof-symbol": "^7.7.4",
- "@babel/plugin-transform-unicode-regex": "^7.7.4",
- "@babel/types": "^7.7.4",
- "browserslist": "^4.6.0",
- "core-js-compat": "^3.6.0",
- "invariant": "^2.2.2",
- "js-levenshtein": "^1.1.3",
- "semver": "^5.5.0"
- },
"dependencies": {
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- }
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/template": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
- "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz",
+ "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==",
"dev": true,
- "requires": {
- "@babel/code-frame": "^7.0.0",
- "@babel/parser": "^7.2.2",
- "@babel/types": "^7.2.2"
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/traverse": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz",
- "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==",
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz",
+ "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==",
"dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/generator": "^7.8.4",
- "@babel/helper-function-name": "^7.8.3",
- "@babel/helper-split-export-declaration": "^7.8.3",
- "@babel/parser": "^7.8.4",
- "@babel/types": "^7.8.3",
- "debug": "^4.1.0",
- "globals": "^11.1.0",
- "lodash": "^4.17.13"
- },
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/generator": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz",
- "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.8.3",
- "jsesc": "^2.5.1",
- "lodash": "^4.17.13",
- "source-map": "^0.5.0"
- }
- },
- "@babel/helper-function-name": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
- "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
- "dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.8.3",
- "@babel/template": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/helper-get-function-arity": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
- "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
- "dev": true
- },
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
- }
- },
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
- }
- },
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- }
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
}
},
- "@babel/types": {
- "version": "7.3.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
- "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+ "node_modules/@babel/preset-env": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.6.tgz",
+ "integrity": "sha512-0gQJ9RTzO0heXOhzftog+a/WyOuqMrAIugVYxMYf83gh1CQaQDjMtsOpqOwXyDL/5JcWsrCm8l4ju8QC97O7EQ==",
"dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.11",
- "to-fast-properties": "^2.0.0"
- },
"dependencies": {
- "to-fast-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
- "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
- "dev": true
- }
+ "@babel/compat-data": "^7.9.6",
+ "@babel/helper-compilation-targets": "^7.9.6",
+ "@babel/helper-module-imports": "^7.8.3",
+ "@babel/helper-plugin-utils": "^7.8.3",
+ "@babel/plugin-proposal-async-generator-functions": "^7.8.3",
+ "@babel/plugin-proposal-dynamic-import": "^7.8.3",
+ "@babel/plugin-proposal-json-strings": "^7.8.3",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-proposal-numeric-separator": "^7.8.3",
+ "@babel/plugin-proposal-object-rest-spread": "^7.9.6",
+ "@babel/plugin-proposal-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-proposal-optional-chaining": "^7.9.0",
+ "@babel/plugin-proposal-unicode-property-regex": "^7.8.3",
+ "@babel/plugin-syntax-async-generators": "^7.8.0",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.0",
+ "@babel/plugin-syntax-json-strings": "^7.8.0",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.0",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.0",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.0",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3",
+ "@babel/plugin-transform-arrow-functions": "^7.8.3",
+ "@babel/plugin-transform-async-to-generator": "^7.8.3",
+ "@babel/plugin-transform-block-scoped-functions": "^7.8.3",
+ "@babel/plugin-transform-block-scoping": "^7.8.3",
+ "@babel/plugin-transform-classes": "^7.9.5",
+ "@babel/plugin-transform-computed-properties": "^7.8.3",
+ "@babel/plugin-transform-destructuring": "^7.9.5",
+ "@babel/plugin-transform-dotall-regex": "^7.8.3",
+ "@babel/plugin-transform-duplicate-keys": "^7.8.3",
+ "@babel/plugin-transform-exponentiation-operator": "^7.8.3",
+ "@babel/plugin-transform-for-of": "^7.9.0",
+ "@babel/plugin-transform-function-name": "^7.8.3",
+ "@babel/plugin-transform-literals": "^7.8.3",
+ "@babel/plugin-transform-member-expression-literals": "^7.8.3",
+ "@babel/plugin-transform-modules-amd": "^7.9.6",
+ "@babel/plugin-transform-modules-commonjs": "^7.9.6",
+ "@babel/plugin-transform-modules-systemjs": "^7.9.6",
+ "@babel/plugin-transform-modules-umd": "^7.9.0",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3",
+ "@babel/plugin-transform-new-target": "^7.8.3",
+ "@babel/plugin-transform-object-super": "^7.8.3",
+ "@babel/plugin-transform-parameters": "^7.9.5",
+ "@babel/plugin-transform-property-literals": "^7.8.3",
+ "@babel/plugin-transform-regenerator": "^7.8.7",
+ "@babel/plugin-transform-reserved-words": "^7.8.3",
+ "@babel/plugin-transform-shorthand-properties": "^7.8.3",
+ "@babel/plugin-transform-spread": "^7.8.3",
+ "@babel/plugin-transform-sticky-regex": "^7.8.3",
+ "@babel/plugin-transform-template-literals": "^7.8.3",
+ "@babel/plugin-transform-typeof-symbol": "^7.8.4",
+ "@babel/plugin-transform-unicode-regex": "^7.8.3",
+ "@babel/preset-modules": "^0.1.3",
+ "@babel/types": "^7.9.6",
+ "browserslist": "^4.11.1",
+ "core-js-compat": "^3.6.2",
+ "invariant": "^2.2.2",
+ "levenary": "^1.1.1",
+ "semver": "^5.5.0"
}
},
- "@istanbuljs/schema": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
- "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
- "dev": true
+ "node_modules/@babel/preset-env/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
},
- "@ngtools/webpack": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.0.1.tgz",
- "integrity": "sha512-SG1MDVSC7pIuaX1QYTh94k/YJa6w2OR2RNbghkDXToDzDv6bKnTQYoJPyXk+gwfDTVD4V5z2dKSNbxFzWleFpg==",
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz",
+ "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==",
"dev": true,
- "requires": {
- "@angular-devkit/core": "9.0.1",
- "enhanced-resolve": "4.1.1",
- "rxjs": "6.5.3",
- "webpack-sources": "1.4.3"
- },
"dependencies": {
- "rxjs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
- "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- }
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
+ "@babel/plugin-transform-dotall-regex": "^7.4.4",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
}
},
- "@schematics/angular": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-9.0.1.tgz",
- "integrity": "sha512-lQ8Qc697ef2jvEf1+tElAUsbOnbUAMo3dnOUVw9RlYO90pHeG3/OdWBMH1kjn3jbjuKuvCVZH3voJUUcLDx6eg==",
+ "node_modules/@babel/runtime": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
+ "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==",
"dev": true,
- "requires": {
- "@angular-devkit/core": "9.0.1",
- "@angular-devkit/schematics": "9.0.1"
+ "dependencies": {
+ "regenerator-runtime": "^0.13.4"
}
},
- "@schematics/update": {
- "version": "0.900.1",
- "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.900.1.tgz",
- "integrity": "sha512-p2xfctTtT5kMAaCTBENxi69m5IhsvdTwwwokb9zVHJYAC6D1K//q1bl30mTe6U2YE3hSPWND2S14ahXw8PyN8g==",
+ "node_modules/@babel/template": {
+ "version": "7.8.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
+ "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==",
"dev": true,
- "requires": {
- "@angular-devkit/core": "9.0.1",
- "@angular-devkit/schematics": "9.0.1",
- "@yarnpkg/lockfile": "1.1.0",
- "ini": "1.3.5",
- "npm-package-arg": "^7.0.0",
- "pacote": "9.5.8",
- "rxjs": "6.5.3",
- "semver": "6.3.0",
- "semver-intersect": "1.4.0"
- },
"dependencies": {
- "npm-package-arg": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz",
- "integrity": "sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==",
- "dev": true,
- "requires": {
- "hosted-git-info": "^3.0.2",
- "osenv": "^0.1.5",
- "semver": "^5.6.0",
- "validate-npm-package-name": "^3.0.0"
- },
- "dependencies": {
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
- }
- },
- "rxjs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
- "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- }
+ "@babel/code-frame": "^7.8.3",
+ "@babel/parser": "^7.8.6",
+ "@babel/types": "^7.8.6"
}
},
- "@types/estree": {
- "version": "0.0.42",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.42.tgz",
- "integrity": "sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ==",
- "dev": true
- },
- "@types/events": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
- "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
- "dev": true
+ "node_modules/@babel/traverse": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz",
+ "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/generator": "^7.11.0",
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/helper-split-export-declaration": "^7.11.0",
+ "@babel/parser": "^7.11.0",
+ "@babel/types": "^7.11.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0",
+ "lodash": "^4.17.19"
+ }
},
- "@types/glob": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
- "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
+ "node_modules/@babel/traverse/node_modules/@babel/generator": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz",
+ "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==",
"dev": true,
- "requires": {
- "@types/events": "*",
- "@types/minimatch": "*",
- "@types/node": "*"
+ "dependencies": {
+ "@babel/types": "^7.11.0",
+ "jsesc": "^2.5.1",
+ "source-map": "^0.5.0"
}
},
- "@types/jasmine": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.12.tgz",
- "integrity": "sha512-lXvr2xFQEVQLkIhuGaR3GC1L9lMU1IxeWnAF/wNY5ZWpC4p9dgxkKkzMp7pntpAdv9pZSnYqgsBkCg32MXSZMg==",
- "dev": true
+ "node_modules/@babel/traverse/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
- "@types/minimatch": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
- "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
- "dev": true
+ "node_modules/@babel/types": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
+ "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "lodash": "^4.17.19",
+ "to-fast-properties": "^2.0.0"
+ }
},
- "@types/node": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-11.12.0.tgz",
- "integrity": "sha512-Lg00egj78gM+4aE0Erw05cuDbvX9sLJbaaPwwRtdCdAMnIudqrQZ0oZX98Ek0yiSK/A2nubHgJfvII/rTT2Dwg==",
- "dev": true
+ "node_modules/@bugsnag/browser": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@bugsnag/browser/-/browser-6.5.2.tgz",
+ "integrity": "sha512-XFKKorJc92ivLnlHHhLiPvkP03tZ5y7n0Z2xO6lOU7t+jWF5YapgwqQAda/TWvyYO38B/baWdnOpWMB3QmjhkA=="
},
- "@types/q": {
- "version": "0.0.32",
- "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
- "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
- "dev": true
+ "node_modules/@bugsnag/js": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@bugsnag/js/-/js-6.5.2.tgz",
+ "integrity": "sha512-4ibw624fM5+Y/WSuo3T/MsJVtslsPV8X0MxFuRxdvpKVUXX216d8hN8E/bG4hr7aipqQOGhBYDqSzeL2wgmh0Q==",
+ "dependencies": {
+ "@bugsnag/browser": "^6.5.2",
+ "@bugsnag/node": "^6.5.2"
+ }
},
- "@types/selenium-webdriver": {
- "version": "3.0.15",
- "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.15.tgz",
- "integrity": "sha512-5nh8/K2u9p4bk95GGCJB7KBvewaB0TUziZ9DTr+mR2I6RoO4OJVqx7rxK83hs2J1tomwtCGkhiW+Dy8EUnfB+Q==",
- "dev": true
+ "node_modules/@bugsnag/node": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@bugsnag/node/-/node-6.5.2.tgz",
+ "integrity": "sha512-KQ1twKoOttMCYsHv7OXUVsommVcrk6RGQ5YoZGlTbREhccbzsvjbiXPKiY31Qc7OXKvaJwSXhnOKrQTpRleFUg==",
+ "dependencies": {
+ "byline": "^5.0.0",
+ "error-stack-parser": "^2.0.2",
+ "iserror": "^0.0.2",
+ "pump": "^3.0.0",
+ "stack-generator": "^2.0.3"
+ }
},
- "@types/source-list-map": {
+ "node_modules/@istanbuljs/schema": {
"version": "0.1.2",
- "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
- "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
- "dev": true
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
+ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
},
- "@types/webpack-sources": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.6.tgz",
- "integrity": "sha512-FtAWR7wR5ocJ9+nP137DV81tveD/ZgB1sadnJ/axUGM3BUVfRPx8oQNMtv3JNfTeHx3VP7cXiyfR/jmtEsVHsQ==",
+ "node_modules/@jsdevtools/coverage-istanbul-loader": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.3.tgz",
+ "integrity": "sha512-TAdNkeGB5Fe4Og+ZkAr1Kvn9by2sfL44IAHFtxlh1BA1XJ5cLpO9iSNki5opWESv3l3vSHsZ9BNKuqFKbEbFaA==",
"dev": true,
- "requires": {
- "@types/node": "*",
- "@types/source-list-map": "*",
- "source-map": "^0.6.1"
+ "dependencies": {
+ "convert-source-map": "^1.7.0",
+ "istanbul-lib-instrument": "^4.0.1",
+ "loader-utils": "^1.4.0",
+ "merge-source-map": "^1.1.0",
+ "schema-utils": "^2.6.4"
+ }
+ },
+ "node_modules/@jsdevtools/coverage-istanbul-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
},
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/@jsdevtools/coverage-istanbul-loader/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
"dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- }
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
}
},
- "@webassemblyjs/ast": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
- "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==",
+ "node_modules/@mrmlnc/readdir-enhanced": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
+ "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==",
"dev": true,
- "requires": {
- "@webassemblyjs/helper-module-context": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/wast-parser": "1.8.5"
+ "dependencies": {
+ "call-me-maybe": "^1.0.1",
+ "glob-to-regexp": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=4"
}
},
- "@webassemblyjs/floating-point-hex-parser": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz",
- "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==",
- "dev": true
+ "node_modules/@ng-toolkit/_utils": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/@ng-toolkit/_utils/-/_utils-8.0.4.tgz",
+ "integrity": "sha512-UhFtW5XWhmTgg0KtS5i1t2VBCoqhRRLw/JBP2Q3EKMHAvNiX8AqJ/O23fo3RMzJfhdOINXpTQcT2KORdi0m83A==",
+ "dependencies": {
+ "@angular-devkit/core": "^8.3.21",
+ "@angular-devkit/schematics": "^8.3.21",
+ "@bugsnag/js": "^6.5.0",
+ "@schematics/angular": "^8.3.21",
+ "js-yaml": "^3.13.1",
+ "outdent": "^0.7.0"
+ }
},
- "@webassemblyjs/helper-api-error": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz",
- "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==",
- "dev": true
+ "node_modules/@ng-toolkit/_utils/node_modules/@angular-devkit/core": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.29.tgz",
+ "integrity": "sha512-4jdja9QPwR6XG14ZSunyyOWT3nE2WtZC5IMDIBZADxujXvhzOU0n4oWpy6/JVHLUAxYNNgzLz+/LQORRWndcPg==",
+ "dependencies": {
+ "ajv": "6.12.3",
+ "fast-json-stable-stringify": "2.0.0",
+ "magic-string": "0.25.3",
+ "rxjs": "6.4.0",
+ "source-map": "0.7.3"
+ },
+ "engines": {
+ "node": ">= 10.9.0",
+ "npm": ">= 6.2.0"
+ }
},
- "@webassemblyjs/helper-buffer": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz",
- "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==",
- "dev": true
+ "node_modules/@ng-toolkit/_utils/node_modules/@angular-devkit/schematics": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.29.tgz",
+ "integrity": "sha512-AFJ9EK0XbcNlO5Dm9vr0OlBo1Nw6AaFXPR+DmHGBdcDDHxqEmYYLWfT+JU/8U2YFIdgrtlwvdtf6UQ3V2jdz1g==",
+ "dependencies": {
+ "@angular-devkit/core": "8.3.29",
+ "rxjs": "6.4.0"
+ },
+ "engines": {
+ "node": ">= 10.9.0",
+ "npm": ">= 6.2.0"
+ }
},
- "@webassemblyjs/helper-code-frame": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz",
- "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==",
- "dev": true,
- "requires": {
- "@webassemblyjs/wast-printer": "1.8.5"
+ "node_modules/@ng-toolkit/_utils/node_modules/@schematics/angular": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.29.tgz",
+ "integrity": "sha512-If+UhCsQzCgnQymiiF8dQRoic34+RgJ6rV0n4k7Tm4N2xNYJOG7ajjzKM7PIeafsF50FKnFP8dqaNGxCMyq5Ew==",
+ "dependencies": {
+ "@angular-devkit/core": "8.3.29",
+ "@angular-devkit/schematics": "8.3.29"
+ },
+ "engines": {
+ "node": ">= 10.9.0",
+ "npm": ">= 6.2.0"
}
},
- "@webassemblyjs/helper-fsm": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz",
- "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==",
- "dev": true
+ "node_modules/@ng-toolkit/_utils/node_modules/fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
},
- "@webassemblyjs/helper-module-context": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz",
- "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "mamacro": "^0.0.3"
+ "node_modules/@ng-toolkit/_utils/node_modules/magic-string": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz",
+ "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.4"
}
},
- "@webassemblyjs/helper-wasm-bytecode": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz",
- "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==",
- "dev": true
+ "node_modules/@ng-toolkit/_utils/node_modules/rxjs": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
+ "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
},
- "@webassemblyjs/helper-wasm-section": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz",
- "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-buffer": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/wasm-gen": "1.8.5"
+ "node_modules/@ng-toolkit/_utils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@ng-toolkit/universal": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@ng-toolkit/universal/-/universal-8.1.0.tgz",
+ "integrity": "sha512-B6lt06A1kZmhD4dTEv3ztmtpT5jtb64WKrlmoStRlG8UfX/c5hB2knTgwWA9jEEXJyjuH+rS0fdC+DLU+csp1w==",
+ "dependencies": {
+ "@angular-devkit/core": "^8.3.3",
+ "@angular-devkit/schematics": "^8.3.3",
+ "@bugsnag/js": "^6.4.0",
+ "@ng-toolkit/_utils": "8.0.4",
+ "@nguniversal/express-engine": "^8.1.1",
+ "@schematics/angular": "^8.3.3",
+ "tslib": "^1.9.0"
}
},
- "@webassemblyjs/ieee754": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz",
- "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==",
+ "node_modules/@ng-toolkit/universal/node_modules/@angular-devkit/core": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.29.tgz",
+ "integrity": "sha512-4jdja9QPwR6XG14ZSunyyOWT3nE2WtZC5IMDIBZADxujXvhzOU0n4oWpy6/JVHLUAxYNNgzLz+/LQORRWndcPg==",
+ "dependencies": {
+ "ajv": "6.12.3",
+ "fast-json-stable-stringify": "2.0.0",
+ "magic-string": "0.25.3",
+ "rxjs": "6.4.0",
+ "source-map": "0.7.3"
+ },
+ "engines": {
+ "node": ">= 10.9.0",
+ "npm": ">= 6.2.0"
+ }
+ },
+ "node_modules/@ng-toolkit/universal/node_modules/@angular-devkit/schematics": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.29.tgz",
+ "integrity": "sha512-AFJ9EK0XbcNlO5Dm9vr0OlBo1Nw6AaFXPR+DmHGBdcDDHxqEmYYLWfT+JU/8U2YFIdgrtlwvdtf6UQ3V2jdz1g==",
+ "dependencies": {
+ "@angular-devkit/core": "8.3.29",
+ "rxjs": "6.4.0"
+ },
+ "engines": {
+ "node": ">= 10.9.0",
+ "npm": ">= 6.2.0"
+ }
+ },
+ "node_modules/@ng-toolkit/universal/node_modules/@nguniversal/express-engine": {
+ "version": "8.2.6",
+ "resolved": "https://registry.npmjs.org/@nguniversal/express-engine/-/express-engine-8.2.6.tgz",
+ "integrity": "sha512-IKUKTpesgjYyB0Xg+fFhSbwbGBJhG0Wfn8MkQAi9RgSi8QsrSMkI3oUXc86Z7fpQL55D/ZIH7PekoC0Fmh/kxA=="
+ },
+ "node_modules/@ng-toolkit/universal/node_modules/@schematics/angular": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.29.tgz",
+ "integrity": "sha512-If+UhCsQzCgnQymiiF8dQRoic34+RgJ6rV0n4k7Tm4N2xNYJOG7ajjzKM7PIeafsF50FKnFP8dqaNGxCMyq5Ew==",
+ "dependencies": {
+ "@angular-devkit/core": "8.3.29",
+ "@angular-devkit/schematics": "8.3.29"
+ },
+ "engines": {
+ "node": ">= 10.9.0",
+ "npm": ">= 6.2.0"
+ }
+ },
+ "node_modules/@ng-toolkit/universal/node_modules/fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+ },
+ "node_modules/@ng-toolkit/universal/node_modules/magic-string": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz",
+ "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "node_modules/@ng-toolkit/universal/node_modules/rxjs": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
+ "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@ng-toolkit/universal/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@ngtools/webpack": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.0.6.tgz",
+ "integrity": "sha512-AbSDhPmsljkZO2jHFpge/5AHLQIrbscWgo4brrhF7NQ5TvPgE0Xn0wU7gxB9++hVUKQLGnnbAvewJyB/uYb9Nw==",
"dev": true,
- "requires": {
- "@xtuc/ieee754": "^1.2.0"
+ "dependencies": {
+ "@angular-devkit/core": "10.0.6",
+ "enhanced-resolve": "4.1.1",
+ "rxjs": "6.5.5",
+ "webpack-sources": "1.4.3"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
}
},
- "@webassemblyjs/leb128": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz",
- "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==",
+ "node_modules/@ngtools/webpack/node_modules/rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
"dev": true,
- "requires": {
- "@xtuc/long": "4.2.2"
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
}
},
- "@webassemblyjs/utf8": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz",
- "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==",
+ "node_modules/@ngtools/webpack/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
"dev": true
},
- "@webassemblyjs/wasm-edit": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz",
- "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==",
+ "node_modules/@nguniversal/builders": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@nguniversal/builders/-/builders-10.1.0.tgz",
+ "integrity": "sha512-4GeQ9S7fVMRbj5bwjCE9VVstrYW3MFrqyIwFcbI/l5Oq1kzWFQ3B6hDX1CVEKQYiofgIi1OWDWAhr/ryrQj1yg==",
"dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-buffer": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/helper-wasm-section": "1.8.5",
- "@webassemblyjs/wasm-gen": "1.8.5",
- "@webassemblyjs/wasm-opt": "1.8.5",
- "@webassemblyjs/wasm-parser": "1.8.5",
- "@webassemblyjs/wast-printer": "1.8.5"
+ "dependencies": {
+ "@angular-devkit/architect": "^0.1001.0",
+ "@angular-devkit/core": "^10.1.0",
+ "browser-sync": "^2.26.7",
+ "guess-parser": "^0.4.12",
+ "http-proxy-middleware": "^1.0.0",
+ "rxjs": "^6.5.5",
+ "tree-kill": "^1.2.1"
}
},
- "@webassemblyjs/wasm-gen": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz",
- "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==",
+ "node_modules/@nguniversal/builders/node_modules/@angular-devkit/architect": {
+ "version": "0.1001.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1001.6.tgz",
+ "integrity": "sha512-Wy10cGRdZ/g+akXbWfv0sq/pjVJrhrilSChe03ovu8nOsbcyZp76z+rnqf3YBYN6yZpWaBB80cW4QC/ar7Kv4Q==",
"dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/ieee754": "1.8.5",
- "@webassemblyjs/leb128": "1.8.5",
- "@webassemblyjs/utf8": "1.8.5"
+ "dependencies": {
+ "@angular-devkit/core": "10.1.6",
+ "rxjs": "6.6.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
}
},
- "@webassemblyjs/wasm-opt": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz",
- "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==",
+ "node_modules/@nguniversal/builders/node_modules/@angular-devkit/core": {
+ "version": "10.1.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.6.tgz",
+ "integrity": "sha512-RhZCbX2I+ukR6/yu1OxwtyveBkQy+knRSQ7oxsBbwkS4M0XzmUswlf0p8lTfJI9pxrJnc2SODatMfEKeOYWmkA==",
"dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-buffer": "1.8.5",
- "@webassemblyjs/wasm-gen": "1.8.5",
- "@webassemblyjs/wasm-parser": "1.8.5"
+ "dependencies": {
+ "ajv": "6.12.4",
+ "fast-json-stable-stringify": "2.1.0",
+ "magic-string": "0.25.7",
+ "rxjs": "6.6.2",
+ "source-map": "0.7.3"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
}
},
- "@webassemblyjs/wasm-parser": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz",
- "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==",
+ "node_modules/@nguniversal/builders/node_modules/ajv": {
+ "version": "6.12.4",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
+ "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==",
"dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-api-error": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/ieee754": "1.8.5",
- "@webassemblyjs/leb128": "1.8.5",
- "@webassemblyjs/utf8": "1.8.5"
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
}
},
- "@webassemblyjs/wast-parser": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz",
- "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==",
+ "node_modules/@nguniversal/builders/node_modules/http-proxy-middleware": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz",
+ "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==",
"dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/floating-point-hex-parser": "1.8.5",
- "@webassemblyjs/helper-api-error": "1.8.5",
- "@webassemblyjs/helper-code-frame": "1.8.5",
- "@webassemblyjs/helper-fsm": "1.8.5",
- "@xtuc/long": "4.2.2"
+ "dependencies": {
+ "@types/http-proxy": "^1.17.4",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "lodash": "^4.17.20",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
}
},
- "@webassemblyjs/wast-printer": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz",
- "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/wast-parser": "1.8.5",
- "@xtuc/long": "4.2.2"
+ "node_modules/@nguniversal/common": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@nguniversal/common/-/common-10.1.0.tgz",
+ "integrity": "sha512-AIfLORs+LLHx9d+8kRNDq+GZj/2ToyXgg5Boi2RfgUhV5Rywey082XRlFmPwyVHxltYJzoMPeNWxzV6hrSMCzA==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
}
},
- "@xtuc/ieee754": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
- "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
- "dev": true
+ "node_modules/@nguniversal/express-engine": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@nguniversal/express-engine/-/express-engine-10.1.0.tgz",
+ "integrity": "sha512-UYQB8662Qnx9Y2TblZmC8QbfAZtiCE6OeLNdwWIz8rVY9jhWi4P5SFb0slvcPMyPL5JAb+FHHOKjsH1NJztsCQ==",
+ "dependencies": {
+ "@nguniversal/common": "10.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
},
- "@xtuc/long": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
- "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
- "dev": true
+ "node_modules/@ngx-utils/cookies": {
+ "resolved": "https://github.com/kenkeiras/ngx-cookies/releases/download/angular-10-support/ngx-cookies-angular-10.tgz",
+ "integrity": "sha512-4x7N5Yb2k364mBDqDgyRzxZOacDdS6yPpMvGTbgt21e4mY3NJFdb+MTjwno1wslt8wA4y8TEv8vM0ThO2pjBLQ=="
},
- "@yarnpkg/lockfile": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
- "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
- "dev": true
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
+ "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.3",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
},
- "JSONStream": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
- "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
+ "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
"dev": true,
- "requires": {
- "jsonparse": "^1.2.0",
- "through": ">=2.2.7 <3"
+ "engines": {
+ "node": ">= 8"
}
},
- "abbrev": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
- "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
- "dev": true
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
+ "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.3",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
},
- "accepts": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
- "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+ "node_modules/@npmcli/move-file": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz",
+ "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==",
"dev": true,
- "requires": {
- "mime-types": "~2.1.18",
- "negotiator": "0.6.1"
+ "dependencies": {
+ "mkdirp": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=10"
}
},
- "acorn": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
- "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
- "dev": true
+ "node_modules/@npmcli/move-file/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
},
- "adm-zip": {
- "version": "0.4.13",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz",
- "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==",
- "dev": true
+ "node_modules/@schematics/angular": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-10.0.6.tgz",
+ "integrity": "sha512-TPBpo0GnMJLvKE6rYZDkSy9pnkMH55rSJ6nfLDpQ5zzmhoD/QnASUr8trfTFs3+MqmPlX61xI00+HmStmI8sJQ==",
+ "dev": true,
+ "dependencies": {
+ "@angular-devkit/core": "10.0.6",
+ "@angular-devkit/schematics": "10.0.6"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
+ }
},
- "after": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
- "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
+ "node_modules/@schematics/update": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.1000.6.tgz",
+ "integrity": "sha512-GGfPGPjRF/MA4EeJ+h1ebzoYDzChF4BV7SaTfpT107LPCD3McRjKS39Jw2qH/ArGNSbrbJ8fYNOIj3g/uh1GoA==",
+ "dev": true,
+ "dependencies": {
+ "@angular-devkit/core": "10.0.6",
+ "@angular-devkit/schematics": "10.0.6",
+ "@yarnpkg/lockfile": "1.1.0",
+ "ini": "1.3.5",
+ "npm-package-arg": "^8.0.0",
+ "pacote": "9.5.12",
+ "rxjs": "6.5.5",
+ "semver": "7.3.2",
+ "semver-intersect": "1.4.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0",
+ "npm": ">= 6.11.0",
+ "yarn": ">= 1.13.0"
+ }
+ },
+ "node_modules/@schematics/update/node_modules/rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/@schematics/update/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
"dev": true
},
- "agent-base": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
- "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+ "node_modules/@sindresorhus/is": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
"dev": true,
- "requires": {
- "es6-promisify": "^5.0.0"
+ "engines": {
+ "node": ">=6"
}
},
- "agentkeepalive": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz",
- "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==",
+ "node_modules/@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+ "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
"dev": true,
- "requires": {
- "humanize-ms": "^1.2.1"
+ "dependencies": {
+ "defer-to-connect": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=6"
}
},
- "aggregate-error": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
- "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
+ "node_modules/@types/body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/color-name": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+ "dev": true
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.33",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
+ "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cookie-parser": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz",
+ "integrity": "sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.8",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz",
+ "integrity": "sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "*",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.17.13",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz",
+ "integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*"
+ }
+ },
+ "node_modules/@types/glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==",
"dev": true,
- "requires": {
- "clean-stack": "^2.0.0",
- "indent-string": "^4.0.0"
+ "dependencies": {
+ "@types/minimatch": "*",
+ "@types/node": "*"
}
},
- "ajv": {
- "version": "6.6.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
- "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.4",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
+ "integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
"dev": true,
- "requires": {
- "fast-deep-equal": "^2.0.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "dependencies": {
+ "@types/node": "*"
}
},
- "ajv-errors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
- "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
+ "node_modules/@types/jasmine": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.12.tgz",
+ "integrity": "sha512-vJaQ58oceFao+NzpKNqLOWwHPsqA7YEhKv+mOXvYU4/qh+BfVWIxaBtL0Ck5iCS67yOkNwGkDCrzepnzIWF+7g==",
"dev": true
},
- "ajv-keywords": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz",
- "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==",
+ "node_modules/@types/json-schema": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
+ "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
"dev": true
},
- "alphanum-sort": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
- "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
+ "node_modules/@types/mime": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
+ "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q=="
+ },
+ "node_modules/@types/minimatch": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
- "amdefine": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
- "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+ "node_modules/@types/node": {
+ "version": "11.15.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.20.tgz",
+ "integrity": "sha512-DY2QwdrBqNlsxdMehwzUtSsWHgYYPLVCAuXvOcu3wkzYmchbRunQ7OEZFOrmFoBLfA1ysz2Ypr6vtNP9WQkUaQ=="
+ },
+ "node_modules/@types/q": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
+ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==",
"dev": true
},
- "ansi-colors": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
- "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
+ "node_modules/@types/qs": {
+ "version": "6.9.5",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
+ "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
+ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
+ },
+ "node_modules/@types/selenium-webdriver": {
+ "version": "3.0.17",
+ "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz",
+ "integrity": "sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw==",
"dev": true
},
- "ansi-escapes": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
- "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==",
- "dev": true,
- "requires": {
- "type-fest": "^0.8.1"
+ "node_modules/@types/serve-static": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz",
+ "integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==",
+ "dependencies": {
+ "@types/express-serve-static-core": "*",
+ "@types/mime": "*"
}
},
- "ansi-html": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
- "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+ "node_modules/@types/source-list-map": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
+ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
+ "node_modules/@types/webpack-sources": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz",
+ "integrity": "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/source-list-map": "*",
+ "source-map": "^0.6.1"
+ }
},
- "ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "node_modules/@types/webpack-sources/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
- "requires": {
- "color-convert": "^1.9.0"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "anymatch": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
- "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
+ "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==",
"dev": true,
- "requires": {
- "micromatch": "^3.1.4",
- "normalize-path": "^2.1.1"
+ "dependencies": {
+ "@webassemblyjs/helper-module-context": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/wast-parser": "1.9.0"
}
},
- "app-root-path": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz",
- "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==",
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz",
+ "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==",
"dev": true
},
- "append-transform": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz",
- "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==",
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz",
+ "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==",
+ "dev": true
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz",
+ "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==",
+ "dev": true
+ },
+ "node_modules/@webassemblyjs/helper-code-frame": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz",
+ "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==",
"dev": true,
- "requires": {
- "default-require-extensions": "^2.0.0"
+ "dependencies": {
+ "@webassemblyjs/wast-printer": "1.9.0"
}
},
- "aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+ "node_modules/@webassemblyjs/helper-fsm": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz",
+ "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==",
"dev": true
},
- "are-we-there-yet": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
- "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+ "node_modules/@webassemblyjs/helper-module-context": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz",
+ "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==",
"dev": true,
- "requires": {
- "delegates": "^1.0.0",
- "readable-stream": "^2.0.6"
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0"
}
},
- "arg": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
- "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==",
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz",
+ "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==",
"dev": true
},
- "argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz",
+ "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==",
"dev": true,
- "requires": {
- "sprintf-js": "~1.0.2"
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-buffer": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/wasm-gen": "1.9.0"
}
},
- "aria-query": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz",
- "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=",
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz",
+ "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==",
"dev": true,
- "requires": {
- "ast-types-flow": "0.0.7",
- "commander": "^2.11.0"
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
}
},
- "arr-diff": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
- "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
- "dev": true
- },
- "arr-flatten": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
- "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
- "dev": true
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz",
+ "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==",
+ "dev": true,
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
},
- "arr-union": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
- "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz",
+ "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==",
"dev": true
},
- "array-flatten": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
- "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
- "dev": true
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz",
+ "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-buffer": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/helper-wasm-section": "1.9.0",
+ "@webassemblyjs/wasm-gen": "1.9.0",
+ "@webassemblyjs/wasm-opt": "1.9.0",
+ "@webassemblyjs/wasm-parser": "1.9.0",
+ "@webassemblyjs/wast-printer": "1.9.0"
+ }
},
- "array-union": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
- "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz",
+ "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==",
"dev": true,
- "requires": {
- "array-uniq": "^1.0.1"
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/ieee754": "1.9.0",
+ "@webassemblyjs/leb128": "1.9.0",
+ "@webassemblyjs/utf8": "1.9.0"
}
},
- "array-uniq": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
- "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
- "dev": true
- },
- "array-unique": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
- "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
- "dev": true
- },
- "arraybuffer.slice": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
- "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
- "dev": true
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz",
+ "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-buffer": "1.9.0",
+ "@webassemblyjs/wasm-gen": "1.9.0",
+ "@webassemblyjs/wasm-parser": "1.9.0"
+ }
},
- "arrify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
- "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
- "dev": true
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz",
+ "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-api-error": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/ieee754": "1.9.0",
+ "@webassemblyjs/leb128": "1.9.0",
+ "@webassemblyjs/utf8": "1.9.0"
+ }
},
- "asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
- "dev": true
+ "node_modules/@webassemblyjs/wast-parser": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz",
+ "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/floating-point-hex-parser": "1.9.0",
+ "@webassemblyjs/helper-api-error": "1.9.0",
+ "@webassemblyjs/helper-code-frame": "1.9.0",
+ "@webassemblyjs/helper-fsm": "1.9.0",
+ "@xtuc/long": "4.2.2"
+ }
},
- "asn1": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
- "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz",
+ "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==",
"dev": true,
- "requires": {
- "safer-buffer": "~2.1.0"
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/wast-parser": "1.9.0",
+ "@xtuc/long": "4.2.2"
}
},
- "asn1.js": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
- "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "node_modules/@wessberg/ts-evaluator": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@wessberg/ts-evaluator/-/ts-evaluator-0.0.26.tgz",
+ "integrity": "sha512-2ktA630RnL6cIF3mHhHwjexvpl/mlvMJWxwMDdL8s5lWLFdby/7VJ2h2iFxosQu/l2cejI2zjXOieCLnSXt6Qg==",
"dev": true,
- "requires": {
- "bn.js": "^4.0.0",
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0"
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "jsdom": "^16.3.0",
+ "object-path": "^0.11.4",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10.1.0"
}
},
- "assert": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
- "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
+ "node_modules/@wessberg/ts-evaluator/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
- "requires": {
- "object-assign": "^4.1.1",
- "util": "0.10.3"
+ "dependencies": {
+ "color-convert": "^2.0.1"
},
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@wessberg/ts-evaluator/node_modules/chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "dev": true,
"dependencies": {
- "inherits": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
- "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
- "dev": true
- },
- "util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
- "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
- "dev": true,
- "requires": {
- "inherits": "2.0.1"
- }
- }
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
}
},
- "assert-plus": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
- "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
- "dev": true
+ "node_modules/@wessberg/ts-evaluator/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
},
- "assign-symbols": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
- "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+ "node_modules/@wessberg/ts-evaluator/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
- "ast-types-flow": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
- "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=",
- "dev": true
+ "node_modules/@wessberg/ts-evaluator/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
},
- "async": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
- "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
+ "node_modules/@wessberg/ts-evaluator/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
- "requires": {
- "lodash": "^4.17.11"
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "async-each": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
- "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true
},
- "async-limiter": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
- "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
- "asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "node_modules/@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true
},
- "atob": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
- "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "node_modules/abab": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz",
+ "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==",
"dev": true
},
- "autoprefixer": {
- "version": "9.7.1",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.1.tgz",
- "integrity": "sha512-w3b5y1PXWlhYulevrTJ0lizkQ5CyqfeU6BIRDbuhsMupstHQOeb1Ur80tcB1zxSu7AwyY/qCQ7Vvqklh31ZBFw==",
- "dev": true,
- "requires": {
- "browserslist": "^4.7.2",
- "caniuse-lite": "^1.0.30001006",
- "chalk": "^2.4.2",
- "normalize-range": "^0.1.2",
- "num2fraction": "^1.2.2",
- "postcss": "^7.0.21",
- "postcss-value-parser": "^4.0.2"
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "node_modules/abstract-leveldown": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
+ "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==",
+ "optional": true,
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "immediate": "^3.2.3",
+ "level-concat-iterator": "~2.0.0",
+ "level-supports": "~1.0.0",
+ "xtend": "~4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/abstract-leveldown/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "optional": true,
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
}
},
- "aws-sign2": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
- "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
- "dev": true
+ "node_modules/abstract-leveldown/node_modules/immediate": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz",
+ "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==",
+ "optional": true
},
- "aws4": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
- "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
- "dev": true
+ "node_modules/accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "dependencies": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
},
- "axobject-query": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz",
- "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==",
+ "node_modules/acorn": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+ "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
"dev": true,
- "requires": {
- "ast-types-flow": "0.0.7"
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
}
},
- "babel-code-frame": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
- "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+ "node_modules/acorn-globals": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+ "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
"dev": true,
- "requires": {
- "chalk": "^1.1.3",
- "esutils": "^2.0.2",
- "js-tokens": "^3.0.2"
- },
"dependencies": {
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
- },
- "chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "dev": true,
- "requires": {
- "ansi-styles": "^2.2.1",
- "escape-string-regexp": "^1.0.2",
- "has-ansi": "^2.0.0",
- "strip-ansi": "^3.0.0",
- "supports-color": "^2.0.0"
- }
- },
- "supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
- "dev": true
- }
+ "acorn": "^7.1.1",
+ "acorn-walk": "^7.1.1"
}
},
- "babel-loader": {
- "version": "8.0.6",
- "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz",
- "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==",
+ "node_modules/acorn-globals/node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
- "requires": {
- "find-cache-dir": "^2.0.0",
- "loader-utils": "^1.0.2",
- "mkdirp": "^0.5.1",
- "pify": "^4.0.1"
+ "bin": {
+ "acorn": "bin/acorn"
},
- "dependencies": {
- "find-cache-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
- "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
- "dev": true,
- "requires": {
- "commondir": "^1.0.1",
- "make-dir": "^2.0.0",
- "pkg-dir": "^3.0.0"
- }
- }
+ "engines": {
+ "node": ">=0.4.0"
}
},
- "babel-plugin-dynamic-import-node": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz",
- "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==",
+ "node_modules/acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true,
- "requires": {
- "object.assign": "^4.1.0"
+ "engines": {
+ "node": ">=0.4.0"
}
},
- "backo2": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
- "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
- "dev": true
+ "node_modules/adjust-sourcemap-loader": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz",
+ "integrity": "sha512-4hFsTsn58+YjrU9qKzML2JSSDqKvN8mUGQ0nNIrfPi8hmIONT4L3uUaT6MKdMsZ9AjsU6D2xDkZxCkbQPxChrA==",
+ "dev": true,
+ "dependencies": {
+ "assert": "1.4.1",
+ "camelcase": "5.0.0",
+ "loader-utils": "1.2.3",
+ "object-path": "0.11.4",
+ "regex-parser": "2.2.10"
+ }
},
- "balanced-match": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
+ "node_modules/adjust-sourcemap-loader/node_modules/camelcase": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
+ "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
},
- "base": {
- "version": "0.11.2",
- "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
- "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "node_modules/adjust-sourcemap-loader/node_modules/emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/adjust-sourcemap-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
- "requires": {
- "cache-base": "^1.0.1",
- "class-utils": "^0.3.5",
- "component-emitter": "^1.2.1",
- "define-property": "^1.0.0",
- "isobject": "^3.0.1",
- "mixin-deep": "^1.2.0",
- "pascalcase": "^0.1.1"
- },
"dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^1.0.0"
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "^1.0.0",
- "is-data-descriptor": "^1.0.0",
- "kind-of": "^6.0.2"
- }
- }
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
}
},
- "base64-arraybuffer": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
- "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
- "dev": true
- },
- "base64-js": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
- "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
- "dev": true
- },
- "base64id": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
- "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
- "dev": true
- },
- "batch": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
- "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
- "dev": true
+ "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^2.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
},
- "bcrypt-pbkdf": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
- "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "node_modules/adjust-sourcemap-loader/node_modules/object-path": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz",
+ "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=",
"dev": true,
- "requires": {
- "tweetnacl": "^0.14.3"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "better-assert": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
- "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+ "node_modules/adm-zip": {
+ "version": "0.4.16",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
+ "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
"dev": true,
- "requires": {
- "callsite": "1.0.0"
+ "engines": {
+ "node": ">=0.3.0"
}
},
- "big.js": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
- "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+ "node_modules/after": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
"dev": true
},
- "binary-extensions": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz",
- "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==",
- "dev": true
+ "node_modules/agent-base": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
+ "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+ "dev": true,
+ "dependencies": {
+ "es6-promisify": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
},
- "blob": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
- "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
- "dev": true
+ "node_modules/agentkeepalive": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz",
+ "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==",
+ "dev": true,
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
},
- "blocking-proxy": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
- "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==",
+ "node_modules/aggregate-error": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
+ "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
"dev": true,
- "requires": {
- "minimist": "^1.2.0"
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
},
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.3",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
+ "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
"dependencies": {
- "minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
- "dev": true
- }
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
}
},
- "bluebird": {
- "version": "3.5.3",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
- "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==",
+ "node_modules/ajv-errors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
+ "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
"dev": true
},
- "bn.js": {
- "version": "4.11.8",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
- "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true
},
- "body-parser": {
- "version": "1.18.3",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
- "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "node_modules/alphanum-sort": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
+ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
+ "dev": true
+ },
+ "node_modules/amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true,
- "requires": {
- "bytes": "3.0.0",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "http-errors": "~1.6.3",
- "iconv-lite": "0.4.23",
- "on-finished": "~2.3.0",
- "qs": "6.5.2",
- "raw-body": "2.3.3",
- "type-is": "~1.6.16"
+ "engines": {
+ "node": ">=0.4.2"
}
},
- "bonjour": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
- "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+ "node_modules/ansi-align": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz",
+ "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==",
"dev": true,
- "requires": {
- "array-flatten": "^2.1.0",
- "deep-equal": "^1.0.1",
- "dns-equal": "^1.0.0",
- "dns-txt": "^2.0.2",
- "multicast-dns": "^6.0.1",
- "multicast-dns-service-types": "^1.1.0"
+ "dependencies": {
+ "string-width": "^3.0.0"
}
},
- "boolbase": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
- "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
- "dev": true
- },
- "bootstrap": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz",
- "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag=="
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "node_modules/ansi-colors": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
+ "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
"dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "engines": {
+ "node": ">=6"
}
},
- "braces": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
- "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "node_modules/ansi-escapes": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
+ "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
"dev": true,
- "requires": {
- "arr-flatten": "^1.1.0",
- "array-unique": "^0.3.2",
- "extend-shallow": "^2.0.1",
- "fill-range": "^4.0.0",
- "isobject": "^3.0.1",
- "repeat-element": "^1.1.2",
- "snapdragon": "^0.8.1",
- "snapdragon-node": "^2.0.1",
- "split-string": "^3.0.2",
- "to-regex": "^3.0.1"
- },
"dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- }
+ "type-fest": "^0.11.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "brorand": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
- "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
- "dev": true
+ "node_modules/ansi-html": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
+ "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+ "dev": true,
+ "engines": [
+ "node >= 0.8.0"
+ ],
+ "bin": {
+ "ansi-html": "bin/ansi-html"
+ }
},
- "browserify-aes": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
- "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true,
- "requires": {
- "buffer-xor": "^1.0.3",
- "cipher-base": "^1.0.0",
- "create-hash": "^1.1.0",
- "evp_bytestokey": "^1.0.3",
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "browserify-cipher": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
- "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
- "requires": {
- "browserify-aes": "^1.0.4",
- "browserify-des": "^1.0.0",
- "evp_bytestokey": "^1.0.0"
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
}
},
- "browserify-des": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
- "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "node_modules/anymatch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+ "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
"dev": true,
- "requires": {
- "cipher-base": "^1.0.1",
- "des.js": "^1.0.0",
- "inherits": "^2.0.1",
- "safe-buffer": "^5.1.2"
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
}
},
- "browserify-rsa": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
- "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+ "node_modules/app-root-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
+ "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==",
"dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "randombytes": "^2.0.1"
+ "engines": {
+ "node": ">= 6.0.0"
}
},
- "browserify-sign": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
- "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+ "node_modules/aproba": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+ "dev": true
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+ "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"dev": true,
- "requires": {
- "bn.js": "^4.1.1",
- "browserify-rsa": "^4.0.0",
- "create-hash": "^1.1.0",
- "create-hmac": "^1.1.2",
- "elliptic": "^6.0.0",
- "inherits": "^2.0.1",
- "parse-asn1": "^5.0.0"
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
}
},
- "browserify-zlib": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
- "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
- "dev": true,
- "requires": {
- "pako": "~1.0.5"
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
}
},
- "browserslist": {
- "version": "4.8.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.3.tgz",
- "integrity": "sha512-iU43cMMknxG1ClEZ2MDKeonKE1CCrFVkQK2AqO2YWFmvIrx4JWrvQ4w4hQez6EpVI8rHTtqh/ruHHDHSOKxvUg==",
+ "node_modules/aria-query": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz",
+ "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=",
"dev": true,
- "requires": {
- "caniuse-lite": "^1.0.30001017",
- "electron-to-chromium": "^1.3.322",
- "node-releases": "^1.1.44"
+ "dependencies": {
+ "ast-types-flow": "0.0.7",
+ "commander": "^2.11.0"
}
},
- "browserstack": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.2.tgz",
- "integrity": "sha512-+6AFt9HzhKykcPF79W6yjEUJcdvZOV0lIXdkORXMJftGrDl0OKWqRF4GHqpDNkxiceDT/uB7Fb/aDwktvXX7dg==",
+ "node_modules/arity-n": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz",
+ "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=",
+ "dev": true
+ },
+ "node_modules/arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
"dev": true,
- "requires": {
- "https-proxy-agent": "^2.2.1"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "buffer": {
- "version": "4.9.2",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
- "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+ "node_modules/arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
"dev": true,
- "requires": {
- "base64-js": "^1.0.2",
- "ieee754": "^1.1.4",
- "isarray": "^1.0.0"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "buffer-alloc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
- "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+ "node_modules/arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
"dev": true,
- "requires": {
- "buffer-alloc-unsafe": "^1.1.0",
- "buffer-fill": "^1.0.0"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "buffer-alloc-unsafe": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
- "dev": true
- },
- "buffer-fill": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
- "dev": true
- },
- "buffer-from": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
- "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "node_modules/array-flatten": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
+ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
"dev": true
},
- "buffer-indexof": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
- "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
- "dev": true
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
},
- "buffer-xor": {
+ "node_modules/array-uniq": {
"version": "1.0.3",
- "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
- "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
- "dev": true
+ "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
- "builtin-modules": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
- "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
- "dev": true
+ "node_modules/array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
- "builtin-status-codes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
- "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+ "node_modules/arraybuffer.slice": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+ "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"dev": true
},
- "builtins": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
- "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=",
- "dev": true
+ "node_modules/arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
- "bytes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
- "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true
},
- "cacache": {
- "version": "13.0.1",
- "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
- "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==",
+ "node_modules/asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"dev": true,
- "requires": {
- "chownr": "^1.1.2",
- "figgy-pudding": "^3.5.1",
- "fs-minipass": "^2.0.0",
- "glob": "^7.1.4",
- "graceful-fs": "^4.2.2",
- "infer-owner": "^1.0.4",
- "lru-cache": "^5.1.1",
- "minipass": "^3.0.0",
- "minipass-collect": "^1.0.2",
- "minipass-flush": "^1.0.5",
- "minipass-pipeline": "^1.2.2",
- "mkdirp": "^0.5.1",
- "move-concurrently": "^1.0.1",
- "p-map": "^3.0.0",
- "promise-inflight": "^1.0.1",
- "rimraf": "^2.7.1",
- "ssri": "^7.0.0",
- "unique-filename": "^1.1.1"
- },
"dependencies": {
- "chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "dev": true
- },
- "fs-minipass": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
- "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
- "dev": true,
- "requires": {
- "minipass": "^3.0.0"
- }
- },
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "graceful-fs": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
- "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
- "dev": true
- },
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "requires": {
- "yallist": "^3.0.2"
- },
- "dependencies": {
- "yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
- }
- }
- },
- "minipass": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz",
- "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "rimraf": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
- "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- }
+ "safer-buffer": "~2.1.0"
}
},
- "cache-base": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
- "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "node_modules/asn1.js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dev": true,
- "requires": {
- "collection-visit": "^1.0.0",
- "component-emitter": "^1.2.1",
- "get-value": "^2.0.6",
- "has-value": "^1.0.0",
- "isobject": "^3.0.1",
- "set-value": "^2.0.0",
- "to-object-path": "^0.3.0",
- "union-value": "^1.0.0",
- "unset-value": "^1.0.0"
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
}
},
- "caller-callsite": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
- "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
+ "node_modules/asn1.js/node_modules/bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ },
+ "node_modules/assert": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+ "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
"dev": true,
- "requires": {
- "callsites": "^2.0.0"
+ "dependencies": {
+ "util": "0.10.3"
}
},
- "caller-path": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
- "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
+ "node_modules/assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true,
- "requires": {
- "caller-callsite": "^2.0.0"
+ "engines": {
+ "node": ">=0.8"
}
},
- "callsite": {
+ "node_modules/assign-symbols": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
- "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
- "dev": true
- },
- "callsites": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
- "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
- "dev": true
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "node_modules/ast-types-flow": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
+ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=",
"dev": true
},
- "caniuse-api": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
- "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
+ "node_modules/async": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
+ "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
- "requires": {
- "browserslist": "^4.0.0",
- "caniuse-lite": "^1.0.0",
- "lodash.memoize": "^4.1.2",
- "lodash.uniq": "^4.5.0"
+ "dependencies": {
+ "lodash": "^4.17.14"
}
},
- "caniuse-lite": {
- "version": "1.0.30001020",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001020.tgz",
- "integrity": "sha512-yWIvwA68wRHKanAVS1GjN8vajAv7MBFshullKCeq/eKpK7pJBVDgFFEqvgWTkcP2+wIDeQGYFRXECjKZnLkUjA==",
+ "node_modules/async-each": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
"dev": true
},
- "canonical-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz",
- "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==",
- "dev": true
+ "node_modules/async-each-series": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz",
+ "integrity": "sha1-dhfBkXQB/Yykooqtzj266Yr+tDI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
},
- "caseless": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
- "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "node_modules/async-limiter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
+ "devOptional": true
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
- "chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true,
- "requires": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
+ "bin": {
+ "atob": "bin/atob.js"
},
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "9.8.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz",
+ "integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==",
+ "dev": true,
"dependencies": {
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- }
+ "browserslist": "^4.12.0",
+ "caniuse-lite": "^1.0.30001061",
+ "chalk": "^2.4.2",
+ "normalize-range": "^0.1.2",
+ "num2fraction": "^1.2.2",
+ "postcss": "^7.0.30",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
- "chardet": {
+ "node_modules/aws-sign2": {
"version": "0.7.0",
- "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
- "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/aws4": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
+ "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"dev": true
},
- "chokidar": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
- "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
+ "node_modules/axios": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
+ "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"dev": true,
- "requires": {
- "anymatch": "~3.1.1",
- "braces": "~3.0.2",
- "fsevents": "~2.1.2",
- "glob-parent": "~5.1.0",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.3.0"
- },
"dependencies": {
- "anymatch": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
- "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
- "dev": true,
- "requires": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- }
- },
- "binary-extensions": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
- "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
- "dev": true
- },
- "braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
- "requires": {
- "fill-range": "^7.0.1"
- }
- },
- "fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
- "requires": {
- "to-regex-range": "^5.0.1"
- }
- },
- "fsevents": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
- "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
- "dev": true,
- "optional": true
- },
- "glob-parent": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
- "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
- "dev": true,
- "requires": {
- "is-glob": "^4.0.1"
- }
- },
- "is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "requires": {
- "binary-extensions": "^2.0.0"
- }
- },
- "is-glob": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
- "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.1"
- }
- },
- "is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true
- },
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
- },
- "readdirp": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
- "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
- "dev": true,
- "requires": {
- "picomatch": "^2.0.7"
- }
- },
- "to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "requires": {
- "is-number": "^7.0.0"
- }
- }
+ "follow-redirects": "1.5.10",
+ "is-buffer": "^2.0.2"
}
},
- "chownr": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
- "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
- "dev": true
+ "node_modules/axios/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
},
- "chrome-trace-event": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
- "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==",
+ "node_modules/axios/node_modules/follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dev": true,
- "requires": {
- "tslib": "^1.9.0"
+ "dependencies": {
+ "debug": "=3.1.0"
+ },
+ "engines": {
+ "node": ">=4.0"
}
},
- "cipher-base": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
- "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "node_modules/axios/node_modules/is-buffer": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
"dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
+ "engines": {
+ "node": ">=4"
}
},
- "circular-dependency-plugin": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz",
- "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==",
+ "node_modules/axios/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
- "class-utils": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
- "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "node_modules/axobject-query": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz",
+ "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==",
"dev": true,
- "requires": {
- "arr-union": "^3.1.0",
- "define-property": "^0.2.5",
- "isobject": "^3.0.0",
- "static-extend": "^0.1.1"
+ "dependencies": {
+ "ast-types-flow": "0.0.7"
+ }
+ },
+ "node_modules/babel-loader": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz",
+ "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==",
+ "dev": true,
+ "dependencies": {
+ "find-cache-dir": "^2.1.0",
+ "loader-utils": "^1.4.0",
+ "mkdirp": "^0.5.3",
+ "pify": "^4.0.1",
+ "schema-utils": "^2.6.5"
},
+ "engines": {
+ "node": ">= 6.9"
+ }
+ },
+ "node_modules/babel-loader/node_modules/find-cache-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
+ "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
+ "dev": true,
"dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^0.1.0"
- }
- }
+ "commondir": "^1.0.1",
+ "make-dir": "^2.0.0",
+ "pkg-dir": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
}
},
- "clean-stack": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
- "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
- "dev": true
+ "node_modules/babel-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
},
- "cli-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
- "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "node_modules/babel-loader/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
- "requires": {
- "restore-cursor": "^3.1.0"
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
}
},
- "cli-spinners": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz",
- "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==",
+ "node_modules/babel-plugin-dynamic-import-node": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
+ "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==",
+ "dev": true,
+ "dependencies": {
+ "object.assign": "^4.1.0"
+ }
+ },
+ "node_modules/backo2": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+ "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
"dev": true
},
- "cli-width": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
- "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+ "node_modules/balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
- "cliui": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
- "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+ "node_modules/base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
"dev": true,
- "requires": {
- "string-width": "^2.1.1",
- "strip-ansi": "^4.0.0",
- "wrap-ansi": "^2.0.0"
- },
"dependencies": {
- "ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
- "dev": true,
- "requires": {
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^4.0.0"
- }
- },
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^3.0.0"
- }
- }
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "clone": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
- "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
- "dev": true
- },
- "clone-deep": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
- "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "node_modules/base/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
"dev": true,
- "requires": {
- "is-plain-object": "^2.0.4",
- "kind-of": "^6.0.2",
- "shallow-clone": "^3.0.0"
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "coa": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
- "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
+ "node_modules/base/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
"dev": true,
- "requires": {
- "@types/q": "^1.5.1",
- "chalk": "^2.4.1",
- "q": "^1.1.2"
- },
"dependencies": {
- "@types/q": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
- "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
- "dev": true
- }
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "code-point-at": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
- "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true
- },
- "codelyzer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-5.2.1.tgz",
- "integrity": "sha512-awBZXFcJUyC5HMYXiHzjr3D24tww2l1D1OqtfA9vUhEtYr32a65A+Gblm/OvsO+HuKLYzn8EDMw1inSM3VbxWA==",
+ "node_modules/base/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
"dev": true,
- "requires": {
- "app-root-path": "^2.2.1",
- "aria-query": "^3.0.0",
- "axobject-query": "2.0.2",
- "css-selector-tokenizer": "^0.7.1",
- "cssauron": "^1.4.0",
- "damerau-levenshtein": "^1.0.4",
- "semver-dsl": "^1.0.1",
- "source-map": "^0.5.7",
- "sprintf-js": "^1.1.2"
- },
"dependencies": {
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- },
- "sprintf-js": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
- "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
- "dev": true
- }
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "collection-visit": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
- "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "node_modules/base/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
"dev": true,
- "requires": {
- "map-visit": "^1.0.0",
- "object-visit": "^1.0.0"
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "color": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz",
- "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==",
+ "node_modules/base64-arraybuffer": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+ "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
"dev": true,
- "requires": {
- "color-convert": "^1.9.1",
- "color-string": "^1.5.2"
+ "engines": {
+ "node": ">= 0.6.0"
}
},
- "color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "node_modules/base64-js": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
+ "devOptional": true
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"dev": true,
- "requires": {
- "color-name": "1.1.3"
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
}
},
- "color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "node_modules/batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
"dev": true
},
- "color-string": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
- "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
+ "node_modules/bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
- "requires": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
+ "dependencies": {
+ "tweetnacl": "^0.14.3"
}
},
- "colors": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
- "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
- "dev": true
+ "node_modules/better-assert": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+ "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+ "dev": true,
+ "dependencies": {
+ "callsite": "1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
},
- "combined-stream": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
- "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+ "node_modules/big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true,
- "requires": {
- "delayed-stream": "~1.0.0"
+ "engines": {
+ "node": "*"
}
},
- "commander": {
- "version": "2.17.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
- "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+ "node_modules/binary-extensions": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/blob": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+ "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
"dev": true
},
- "commondir": {
+ "node_modules/blocking-proxy": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
- "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
- "dev": true
+ "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
+ "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "blocking-proxy": "built/lib/bin.js"
+ },
+ "engines": {
+ "node": ">=6.9.x"
+ }
},
- "compare-versions": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.4.0.tgz",
- "integrity": "sha512-tK69D7oNXXqUW3ZNo/z7NXTEz22TCF0pTE+YF9cxvaAM9XnkLo1fV621xCLrRR6aevJlKxExkss0vWqUCUpqdg==",
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
- "component-bind": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
- "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+ "node_modules/bn.js": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
+ "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==",
"dev": true
},
- "component-emitter": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
- "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
- "dev": true
+ "node_modules/body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+ "dependencies": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
},
- "component-inherit": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
- "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
- "dev": true
+ "node_modules/body-parser/node_modules/bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
},
- "compressible": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
- "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
- "dev": true,
- "requires": {
- "mime-db": ">= 1.43.0 < 2"
- },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
- "mime-db": {
- "version": "1.43.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
- "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
- "dev": true
- }
+ "ms": "2.0.0"
}
},
- "compression": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
- "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
- "dev": true,
- "requires": {
- "accepts": "~1.3.5",
- "bytes": "3.0.0",
- "compressible": "~2.0.16",
- "debug": "2.6.9",
- "on-headers": "~1.0.2",
- "safe-buffer": "5.1.2",
- "vary": "~1.1.2"
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
- },
- "concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
- "dev": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
- "typedarray": "^0.0.6"
- }
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
- "connect": {
- "version": "3.6.6",
- "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
- "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+ "node_modules/bonjour": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
+ "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
"dev": true,
- "requires": {
- "debug": "2.6.9",
- "finalhandler": "1.1.0",
- "parseurl": "~1.3.2",
- "utils-merge": "1.0.1"
- },
"dependencies": {
- "finalhandler": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
- "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
- "dev": true,
- "requires": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.1",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.2",
- "statuses": "~1.3.1",
- "unpipe": "~1.0.0"
- }
- },
- "statuses": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
- "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
- "dev": true
- }
+ "array-flatten": "^2.1.0",
+ "deep-equal": "^1.0.1",
+ "dns-equal": "^1.0.0",
+ "dns-txt": "^2.0.2",
+ "multicast-dns": "^6.0.1",
+ "multicast-dns-service-types": "^1.1.0"
}
},
- "connect-history-api-fallback": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
- "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true
},
- "console-browserify": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
- "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
- "dev": true
+ "node_modules/bootstrap": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.2.tgz",
+ "integrity": "sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A=="
},
- "console-control-strings": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
- "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
- "dev": true
+ "node_modules/boxen": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz",
+ "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-align": "^3.0.0",
+ "camelcase": "^5.3.1",
+ "chalk": "^3.0.0",
+ "cli-boxes": "^2.2.0",
+ "string-width": "^4.1.0",
+ "term-size": "^2.1.0",
+ "type-fest": "^0.8.1",
+ "widest-line": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
- "constants-browserify": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
- "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
- "dev": true
+ "node_modules/boxen/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
},
- "content-disposition": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
- "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "node_modules/boxen/node_modules/ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
- "requires": {
- "safe-buffer": "5.1.2"
+ "dependencies": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "content-type": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
- "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
- "dev": true
+ "node_modules/boxen/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
- "convert-source-map": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
- "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
+ "node_modules/boxen/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
- "requires": {
- "safe-buffer": "~5.1.1"
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
}
},
- "cookie": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
- "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+ "node_modules/boxen/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
- "cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+ "node_modules/boxen/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
- "copy-concurrently": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
- "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
+ "node_modules/boxen/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
- "requires": {
- "aproba": "^1.1.1",
- "fs-write-stream-atomic": "^1.0.8",
- "iferr": "^0.1.5",
- "mkdirp": "^0.5.1",
- "rimraf": "^2.5.4",
- "run-queue": "^1.0.0"
+ "engines": {
+ "node": ">=8"
}
},
- "copy-descriptor": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
- "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
- "dev": true
+ "node_modules/boxen/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
},
- "copy-webpack-plugin": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz",
- "integrity": "sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==",
+ "node_modules/boxen/node_modules/string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
- "requires": {
- "cacache": "^12.0.3",
- "find-cache-dir": "^2.1.0",
- "glob-parent": "^3.1.0",
- "globby": "^7.1.1",
- "is-glob": "^4.0.1",
- "loader-utils": "^1.2.3",
- "minimatch": "^3.0.4",
- "normalize-path": "^3.0.0",
- "p-limit": "^2.2.1",
- "schema-utils": "^1.0.0",
- "serialize-javascript": "^2.1.2",
- "webpack-log": "^2.0.0"
- },
"dependencies": {
- "bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
- "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
- },
- "cacache": {
- "version": "12.0.3",
- "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz",
- "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
- "dev": true,
- "requires": {
- "bluebird": "^3.5.5",
- "chownr": "^1.1.1",
- "figgy-pudding": "^3.5.1",
- "glob": "^7.1.4",
- "graceful-fs": "^4.1.15",
- "infer-owner": "^1.0.3",
- "lru-cache": "^5.1.1",
- "mississippi": "^3.0.0",
- "mkdirp": "^0.5.1",
- "move-concurrently": "^1.0.1",
- "promise-inflight": "^1.0.1",
- "rimraf": "^2.6.3",
- "ssri": "^6.0.1",
- "unique-filename": "^1.1.1",
- "y18n": "^4.0.0"
- }
- },
- "find-cache-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
- "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
- "dev": true,
- "requires": {
- "commondir": "^1.0.1",
- "make-dir": "^2.0.0",
- "pkg-dir": "^3.0.0"
- }
- },
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "is-glob": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
- "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.1"
- }
- },
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "requires": {
- "yallist": "^3.0.2"
- }
- },
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
- },
- "ssri": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
- "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
- "dev": true,
- "requires": {
- "figgy-pudding": "^3.5.1"
- }
- },
- "yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
- }
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "core-js": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
- "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A=="
- },
- "core-js-compat": {
- "version": "3.6.4",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.4.tgz",
- "integrity": "sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==",
+ "node_modules/boxen/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
- "requires": {
- "browserslist": "^4.8.3",
- "semver": "7.0.0"
- },
"dependencies": {
- "semver": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
- "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
- "dev": true
- }
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "core-util-is": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
- "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
- "dev": true
- },
- "cosmiconfig": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
- "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
+ "node_modules/boxen/node_modules/supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
- "requires": {
- "import-fresh": "^2.0.0",
- "is-directory": "^0.3.1",
- "js-yaml": "^3.13.1",
- "parse-json": "^4.0.0"
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "coverage-istanbul-loader": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/coverage-istanbul-loader/-/coverage-istanbul-loader-2.0.3.tgz",
- "integrity": "sha512-LiGRvyIuzVYs3M1ZYK1tF0HekjH0DJ8zFdUwAZq378EJzqOgToyb1690dp3TAUlP6Y+82uu42LRjuROVeJ54CA==",
- "dev": true,
- "requires": {
- "convert-source-map": "^1.7.0",
- "istanbul-lib-instrument": "^4.0.0",
- "loader-utils": "^1.2.3",
- "merge-source-map": "^1.1.0",
- "schema-utils": "^2.6.1"
- },
+ "node_modules/boxen/node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
"dependencies": {
- "ajv": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
- "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
- "dev": true
- },
- "schema-utils": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
- "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
- "dev": true,
- "requires": {
- "ajv": "^6.10.2",
- "ajv-keywords": "^3.4.1"
- }
- }
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
}
},
- "create-ecdh": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
- "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "elliptic": "^6.0.0"
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "create-hash": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
- "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "node_modules/brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+ "dev": true
+ },
+ "node_modules/browser-process-hrtime": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
+ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
+ "dev": true
+ },
+ "node_modules/browser-sync": {
+ "version": "2.26.13",
+ "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.26.13.tgz",
+ "integrity": "sha512-JPYLTngIzI+Dzx+StSSlMtF+Q9yjdh58HW6bMFqkFXuzQkJL8FCvp4lozlS6BbECZcsM2Gmlgp0uhEjvl18X4w==",
"dev": true,
- "requires": {
- "cipher-base": "^1.0.1",
- "inherits": "^2.0.1",
- "md5.js": "^1.3.4",
- "ripemd160": "^2.0.1",
- "sha.js": "^2.4.0"
+ "dependencies": {
+ "browser-sync-client": "^2.26.13",
+ "browser-sync-ui": "^2.26.13",
+ "bs-recipes": "1.3.4",
+ "bs-snippet-injector": "^2.0.1",
+ "chokidar": "^3.4.1",
+ "connect": "3.6.6",
+ "connect-history-api-fallback": "^1",
+ "dev-ip": "^1.0.1",
+ "easy-extender": "^2.3.4",
+ "eazy-logger": "3.1.0",
+ "etag": "^1.8.1",
+ "fresh": "^0.5.2",
+ "fs-extra": "3.0.1",
+ "http-proxy": "^1.18.1",
+ "immutable": "^3",
+ "localtunnel": "^2.0.0",
+ "micromatch": "^4.0.2",
+ "opn": "5.3.0",
+ "portscanner": "2.1.1",
+ "qs": "6.2.3",
+ "raw-body": "^2.3.2",
+ "resp-modifier": "6.0.2",
+ "rx": "4.1.0",
+ "send": "0.16.2",
+ "serve-index": "1.9.1",
+ "serve-static": "1.13.2",
+ "server-destroy": "1.0.1",
+ "socket.io": "2.1.1",
+ "ua-parser-js": "^0.7.18",
+ "yargs": "^15.4.1"
+ },
+ "bin": {
+ "browser-sync": "dist/bin.js"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
}
},
- "create-hmac": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
- "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "node_modules/browser-sync-client": {
+ "version": "2.26.13",
+ "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.26.13.tgz",
+ "integrity": "sha512-p2VbZoYrpuDhkreq+/Sv1MkToHklh7T1OaIntDwpG6Iy2q/XkBcgwPcWjX+WwRNiZjN8MEehxIjEUh12LweLmQ==",
"dev": true,
- "requires": {
- "cipher-base": "^1.0.3",
- "create-hash": "^1.1.0",
- "inherits": "^2.0.1",
- "ripemd160": "^2.0.0",
- "safe-buffer": "^5.0.1",
- "sha.js": "^2.4.8"
+ "dependencies": {
+ "etag": "1.8.1",
+ "fresh": "0.5.2",
+ "mitt": "^1.1.3",
+ "rxjs": "^5.5.6"
+ },
+ "engines": {
+ "node": ">=8.0.0"
}
},
- "cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "node_modules/browser-sync-client/node_modules/rxjs": {
+ "version": "5.5.12",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz",
+ "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==",
"dev": true,
- "requires": {
- "nice-try": "^1.0.4",
- "path-key": "^2.0.1",
- "semver": "^5.5.0",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
+ "dependencies": {
+ "symbol-observable": "1.0.1"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
}
},
- "crypto-browserify": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
- "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "node_modules/browser-sync-client/node_modules/symbol-observable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
+ "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=",
"dev": true,
- "requires": {
- "browserify-cipher": "^1.0.0",
- "browserify-sign": "^4.0.0",
- "create-ecdh": "^4.0.0",
- "create-hash": "^1.1.0",
- "create-hmac": "^1.1.0",
- "diffie-hellman": "^5.0.0",
- "inherits": "^2.0.1",
- "pbkdf2": "^3.0.3",
- "public-encrypt": "^4.0.0",
- "randombytes": "^2.0.0",
- "randomfill": "^1.0.3"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "css": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
- "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+ "node_modules/browser-sync-ui": {
+ "version": "2.26.13",
+ "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.26.13.tgz",
+ "integrity": "sha512-6NJ/pCnhCnBMzaty1opWo7ipDmFAIk8U71JMQGKJxblCUaGfdsbF2shf6XNZSkXYia1yS0vwKu9LIOzpXqQZCA==",
"dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "source-map": "^0.6.1",
- "source-map-resolve": "^0.5.2",
- "urix": "^0.1.0"
- },
"dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- }
+ "async-each-series": "0.1.1",
+ "connect-history-api-fallback": "^1",
+ "immutable": "^3",
+ "server-destroy": "1.0.1",
+ "socket.io-client": "^2.0.4",
+ "stream-throttle": "^0.1.3"
}
},
- "css-color-names": {
- "version": "0.0.4",
- "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
- "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
- "dev": true
- },
- "css-declaration-sorter": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz",
- "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==",
+ "node_modules/browser-sync/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
- "requires": {
- "postcss": "^7.0.1",
- "timsort": "^0.3.0"
+ "engines": {
+ "node": ">=8"
}
},
- "css-parse": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
- "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
+ "node_modules/browser-sync/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
- "requires": {
- "css": "^2.0.0"
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "css-select": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
- "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
+ "node_modules/browser-sync/node_modules/base64id": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+ "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
"dev": true,
- "requires": {
- "boolbase": "^1.0.0",
- "css-what": "^3.2.1",
- "domutils": "^1.7.0",
- "nth-check": "^1.0.2"
+ "engines": {
+ "node": ">= 0.4.0"
}
},
- "css-select-base-adapter": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
- "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
- "dev": true
- },
- "css-selector-tokenizer": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz",
- "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==",
+ "node_modules/browser-sync/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dev": true,
- "requires": {
- "cssesc": "^0.1.0",
- "fastparse": "^1.1.1",
- "regexpu-core": "^1.0.0"
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
}
},
- "css-tree": {
- "version": "1.0.0-alpha.37",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
- "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==",
+ "node_modules/browser-sync/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
- "requires": {
- "mdn-data": "2.0.4",
- "source-map": "^0.6.1"
- },
"dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- }
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
}
},
- "css-unit-converter": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz",
- "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=",
+ "node_modules/browser-sync/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
- "css-what": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz",
- "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==",
+ "node_modules/browser-sync/node_modules/component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"dev": true
},
- "cssauron": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz",
- "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=",
+ "node_modules/browser-sync/node_modules/connect": {
+ "version": "3.6.6",
+ "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+ "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
"dev": true,
- "requires": {
- "through": "X.X.X"
+ "dependencies": {
+ "debug": "2.6.9",
+ "finalhandler": "1.1.0",
+ "parseurl": "~1.3.2",
+ "utils-merge": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
}
},
- "cssesc": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz",
- "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=",
+ "node_modules/browser-sync/node_modules/cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/browser-sync/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/browser-sync/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
- "cssnano": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
- "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
+ "node_modules/browser-sync/node_modules/engine.io": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
+ "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==",
"dev": true,
- "requires": {
- "cosmiconfig": "^5.0.0",
- "cssnano-preset-default": "^4.0.7",
- "is-resolvable": "^1.0.0",
- "postcss": "^7.0.0"
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "1.0.0",
+ "cookie": "0.3.1",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.0",
+ "ws": "~3.3.1"
}
},
- "cssnano-preset-default": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz",
- "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==",
+ "node_modules/browser-sync/node_modules/engine.io-client": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+ "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
"dev": true,
- "requires": {
- "css-declaration-sorter": "^4.0.1",
- "cssnano-util-raw-cache": "^4.0.1",
- "postcss": "^7.0.0",
- "postcss-calc": "^7.0.1",
- "postcss-colormin": "^4.0.3",
- "postcss-convert-values": "^4.0.1",
- "postcss-discard-comments": "^4.0.2",
- "postcss-discard-duplicates": "^4.0.2",
- "postcss-discard-empty": "^4.0.1",
- "postcss-discard-overridden": "^4.0.1",
- "postcss-merge-longhand": "^4.0.11",
- "postcss-merge-rules": "^4.0.3",
- "postcss-minify-font-values": "^4.0.2",
- "postcss-minify-gradients": "^4.0.2",
- "postcss-minify-params": "^4.0.2",
- "postcss-minify-selectors": "^4.0.2",
- "postcss-normalize-charset": "^4.0.1",
- "postcss-normalize-display-values": "^4.0.2",
- "postcss-normalize-positions": "^4.0.2",
- "postcss-normalize-repeat-style": "^4.0.2",
- "postcss-normalize-string": "^4.0.2",
- "postcss-normalize-timing-functions": "^4.0.2",
- "postcss-normalize-unicode": "^4.0.1",
- "postcss-normalize-url": "^4.0.1",
- "postcss-normalize-whitespace": "^4.0.2",
- "postcss-ordered-values": "^4.1.2",
- "postcss-reduce-initial": "^4.0.3",
- "postcss-reduce-transforms": "^4.0.2",
- "postcss-svgo": "^4.0.2",
- "postcss-unique-selectors": "^4.0.1"
+ "dependencies": {
+ "component-emitter": "1.2.1",
+ "component-inherit": "0.0.3",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.1",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "ws": "~3.3.1",
+ "xmlhttprequest-ssl": "~1.5.4",
+ "yeast": "0.1.2"
}
},
- "cssnano-util-get-arguments": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz",
- "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=",
- "dev": true
- },
- "cssnano-util-get-match": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz",
- "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=",
- "dev": true
+ "node_modules/browser-sync/node_modules/engine.io-client/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
},
- "cssnano-util-raw-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz",
- "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==",
+ "node_modules/browser-sync/node_modules/engine.io-parser": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+ "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
"dev": true,
- "requires": {
- "postcss": "^7.0.0"
+ "dependencies": {
+ "after": "0.8.2",
+ "arraybuffer.slice": "~0.0.7",
+ "base64-arraybuffer": "0.1.5",
+ "blob": "0.0.5",
+ "has-binary2": "~1.0.2"
}
},
- "cssnano-util-same-parent": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz",
- "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==",
- "dev": true
+ "node_modules/browser-sync/node_modules/engine.io/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
},
- "csso": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.2.tgz",
- "integrity": "sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg==",
+ "node_modules/browser-sync/node_modules/finalhandler": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+ "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
"dev": true,
- "requires": {
- "css-tree": "1.0.0-alpha.37"
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.1",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "statuses": "~1.3.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
}
},
- "custom-event": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
- "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
- "dev": true
+ "node_modules/browser-sync/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
- "cyclist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
- "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
- "dev": true
+ "node_modules/browser-sync/node_modules/fs-extra": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
+ "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^3.0.0",
+ "universalify": "^0.1.0"
+ }
},
- "damerau-levenshtein": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
- "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==",
- "dev": true
+ "node_modules/browser-sync/node_modules/http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "dev": true,
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
},
- "dashdash": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
- "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "node_modules/browser-sync/node_modules/http-errors/node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true,
- "requires": {
- "assert-plus": "^1.0.0"
+ "engines": {
+ "node": ">= 0.6"
}
},
- "date-format": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz",
- "integrity": "sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA==",
+ "node_modules/browser-sync/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "node_modules/browser-sync/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
- "requires": {
- "ms": "2.0.0"
+ "engines": {
+ "node": ">=8"
}
},
- "debuglog": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
- "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=",
- "dev": true
+ "node_modules/browser-sync/node_modules/is-wsl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
},
- "decamelize": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "node_modules/browser-sync/node_modules/isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
"dev": true
},
- "decode-uri-component": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
- "dev": true
+ "node_modules/browser-sync/node_modules/jsonfile": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
+ "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.6"
+ }
},
- "deep-equal": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
- "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
+ "node_modules/browser-sync/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
- "requires": {
- "is-arguments": "^1.0.4",
- "is-date-object": "^1.0.1",
- "is-regex": "^1.0.4",
- "object-is": "^1.0.1",
- "object-keys": "^1.1.1",
- "regexp.prototype.flags": "^1.2.0"
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "default-gateway": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
- "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==",
+ "node_modules/browser-sync/node_modules/mime": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
"dev": true,
- "requires": {
- "execa": "^1.0.0",
- "ip-regex": "^2.1.0"
+ "bin": {
+ "mime": "cli.js"
}
},
- "default-require-extensions": {
+ "node_modules/browser-sync/node_modules/ms": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz",
- "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/browser-sync/node_modules/opn": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
+ "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==",
"dev": true,
- "requires": {
- "strip-bom": "^3.0.0"
- },
"dependencies": {
- "strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
- "dev": true
- }
+ "is-wsl": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
}
},
- "defaults": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
- "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+ "node_modules/browser-sync/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
- "requires": {
- "clone": "^1.0.2"
- },
"dependencies": {
- "clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
- "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
- "dev": true
- }
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "define-properties": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
- "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "node_modules/browser-sync/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
- "requires": {
- "object-keys": "^1.0.12"
+ "engines": {
+ "node": ">=8"
}
},
- "define-property": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
- "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "node_modules/browser-sync/node_modules/qs": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
+ "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=",
"dev": true,
- "requires": {
- "is-descriptor": "^1.0.2",
- "isobject": "^3.0.1"
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/browser-sync/node_modules/send": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+ "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.6.2",
+ "mime": "1.4.1",
+ "ms": "2.0.0",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.0",
+ "statuses": "~1.4.0"
},
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/browser-sync/node_modules/send/node_modules/statuses": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+ "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/browser-sync/node_modules/serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "dev": true,
"dependencies": {
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "^1.0.0",
- "is-data-descriptor": "^1.0.0",
- "kind-of": "^6.0.2"
- }
- }
- }
- },
- "del": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
- "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
- "dev": true,
- "requires": {
- "@types/glob": "^7.1.1",
- "globby": "^6.1.0",
- "is-path-cwd": "^2.0.0",
- "is-path-in-cwd": "^2.0.0",
- "p-map": "^2.0.0",
- "pify": "^4.0.1",
- "rimraf": "^2.6.3"
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.2",
+ "send": "0.16.2"
},
- "dependencies": {
- "globby": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
- "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
- "dev": true,
- "requires": {
- "array-union": "^1.0.1",
- "glob": "^7.0.3",
- "object-assign": "^4.0.1",
- "pify": "^2.0.0",
- "pinkie-promise": "^2.0.0"
- },
- "dependencies": {
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
- }
- },
- "is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
- "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
- "dev": true
- },
- "is-path-in-cwd": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
- "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
- "dev": true,
- "requires": {
- "is-path-inside": "^2.1.0"
- }
- },
- "is-path-inside": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
- "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
- "dev": true,
- "requires": {
- "path-is-inside": "^1.0.2"
- }
- },
- "p-map": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
- "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
- "dev": true
- }
+ "engines": {
+ "node": ">= 0.8.0"
}
},
- "delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
- "dev": true
- },
- "delegates": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
- "dev": true
- },
- "depd": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
- "dev": true
- },
- "dependency-graph": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz",
- "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==",
+ "node_modules/browser-sync/node_modules/setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
"dev": true
},
- "des.js": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
- "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+ "node_modules/browser-sync/node_modules/socket.io": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
+ "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
"dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0"
+ "dependencies": {
+ "debug": "~3.1.0",
+ "engine.io": "~3.2.0",
+ "has-binary2": "~1.0.2",
+ "socket.io-adapter": "~1.1.0",
+ "socket.io-client": "2.1.1",
+ "socket.io-parser": "~3.2.0"
}
},
- "destroy": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
- "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
- "dev": true
- },
- "detect-node": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
- "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
- "dev": true
- },
- "dezalgo": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
- "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
+ "node_modules/browser-sync/node_modules/socket.io-client": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
+ "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
"dev": true,
- "requires": {
- "asap": "^2.0.0",
- "wrappy": "1"
+ "dependencies": {
+ "backo2": "1.0.2",
+ "base64-arraybuffer": "0.1.5",
+ "component-bind": "1.0.0",
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "engine.io-client": "~3.2.0",
+ "has-binary2": "~1.0.2",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "object-component": "0.0.3",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "socket.io-parser": "~3.2.0",
+ "to-array": "0.1.4"
}
},
- "di": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
- "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
- "dev": true
- },
- "diff": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
- "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
- "dev": true
+ "node_modules/browser-sync/node_modules/socket.io-client/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
},
- "diffie-hellman": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
- "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "node_modules/browser-sync/node_modules/socket.io-parser": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+ "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
"dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "miller-rabin": "^4.0.0",
- "randombytes": "^2.0.0"
+ "dependencies": {
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "isarray": "2.0.1"
}
},
- "dir-glob": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
- "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==",
+ "node_modules/browser-sync/node_modules/socket.io-parser/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
- "requires": {
- "path-type": "^3.0.0"
+ "dependencies": {
+ "ms": "2.0.0"
}
},
- "dns-equal": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
- "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
- "dev": true
+ "node_modules/browser-sync/node_modules/socket.io/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
},
- "dns-packet": {
+ "node_modules/browser-sync/node_modules/statuses": {
"version": "1.3.1",
- "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
- "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+ "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
"dev": true,
- "requires": {
- "ip": "^1.1.0",
- "safe-buffer": "^5.0.1"
+ "engines": {
+ "node": ">= 0.6"
}
},
- "dns-txt": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
- "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+ "node_modules/browser-sync/node_modules/string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
- "requires": {
- "buffer-indexof": "^1.0.0"
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "dom-serialize": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
- "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+ "node_modules/browser-sync/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
- "requires": {
- "custom-event": "~1.0.0",
- "ent": "~2.2.0",
- "extend": "^3.0.0",
- "void-elements": "^2.0.0"
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "dom-serializer": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
- "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+ "node_modules/browser-sync/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
- "requires": {
- "domelementtype": "^2.0.1",
- "entities": "^2.0.0"
- },
"dependencies": {
- "domelementtype": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
- "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
- "dev": true
- }
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "domain-browser": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
- "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
- "dev": true
- },
- "domelementtype": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
- "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
- "dev": true
+ "node_modules/browser-sync/node_modules/ws": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+ "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+ "dev": true,
+ "dependencies": {
+ "async-limiter": "~1.0.0",
+ "safe-buffer": "~5.1.0",
+ "ultron": "~1.1.0"
+ }
},
- "domutils": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
- "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+ "node_modules/browser-sync/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dev": true,
- "requires": {
- "dom-serializer": "0",
- "domelementtype": "1"
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "dot-prop": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
- "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
+ "node_modules/browser-sync/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
- "requires": {
- "is-obj": "^1.0.0"
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
}
},
- "duplexify": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
- "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "node_modules/browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
- "requires": {
- "end-of-stream": "^1.0.0",
+ "dependencies": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
"inherits": "^2.0.1",
- "readable-stream": "^2.0.0",
- "stream-shift": "^1.0.0"
+ "safe-buffer": "^5.0.1"
}
},
- "ecc-jsbn": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
- "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "node_modules/browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
"dev": true,
- "requires": {
- "jsbn": "~0.1.0",
- "safer-buffer": "^2.1.0"
+ "dependencies": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
}
},
- "ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
- "dev": true
- },
- "electron-to-chromium": {
- "version": "1.3.349",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.349.tgz",
- "integrity": "sha512-uEb2zs6EJ6OZIqaMsCSliYVgzE/f7/s1fLWqtvRtHg/v5KBF2xds974fUnyatfxIDgkqzQVwFtam5KExqywx0Q==",
- "dev": true
- },
- "elliptic": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
- "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
+ "node_modules/browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
"dev": true,
- "requires": {
- "bn.js": "^4.4.0",
- "brorand": "^1.0.1",
- "hash.js": "^1.0.0",
- "hmac-drbg": "^1.0.0",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
"inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0",
- "minimalistic-crypto-utils": "^1.0.0"
+ "safe-buffer": "^5.1.2"
}
},
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "emojis-list": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
- "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
- "dev": true
+ "node_modules/browserify-rsa": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "randombytes": "^2.0.1"
+ }
},
- "encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+ "node_modules/browserify-rsa/node_modules/bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"dev": true
},
- "encoding": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
- "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+ "node_modules/browserify-sign": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
+ "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
"dev": true,
- "requires": {
- "iconv-lite": "~0.4.13"
+ "dependencies": {
+ "bn.js": "^5.1.1",
+ "browserify-rsa": "^4.0.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.3",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.5",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
}
},
- "end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
- "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "node_modules/browserify-sign/node_modules/readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
- "requires": {
- "once": "^1.4.0"
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
- "engine.io": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
- "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==",
+ "node_modules/browserify-sign/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"dev": true,
- "requires": {
- "accepts": "~1.3.4",
- "base64id": "1.0.0",
- "cookie": "0.3.1",
- "debug": "~3.1.0",
- "engine.io-parser": "~2.1.0",
- "ws": "~3.3.1"
- },
"dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
+ "pako": "~1.0.5"
}
},
- "engine.io-client": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
- "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
+ "node_modules/browserslist": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz",
+ "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==",
"dev": true,
- "requires": {
- "component-emitter": "1.2.1",
- "component-inherit": "0.0.3",
- "debug": "~3.1.0",
- "engine.io-parser": "~2.1.1",
- "has-cors": "1.1.0",
- "indexof": "0.0.1",
- "parseqs": "0.0.5",
- "parseuri": "0.0.5",
- "ws": "~3.3.1",
- "xmlhttprequest-ssl": "~1.5.4",
- "yeast": "0.1.2"
- },
"dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
+ "caniuse-lite": "^1.0.30001111",
+ "electron-to-chromium": "^1.3.523",
+ "escalade": "^3.0.2",
+ "node-releases": "^1.1.60"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "engine.io-parser": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
- "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+ "node_modules/browserstack": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.6.0.tgz",
+ "integrity": "sha512-HJDJ0TSlmkwnt9RZ+v5gFpa1XZTBYTj0ywvLwJ3241J7vMw2jAsGNVhKHtmCOyg+VxeLZyaibO9UL71AsUeDIw==",
"dev": true,
- "requires": {
- "after": "0.8.2",
- "arraybuffer.slice": "~0.0.7",
- "base64-arraybuffer": "0.1.5",
- "blob": "0.0.5",
- "has-binary2": "~1.0.2"
+ "dependencies": {
+ "https-proxy-agent": "^2.2.1"
}
},
- "enhanced-resolve": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz",
- "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==",
+ "node_modules/bs-recipes": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz",
+ "integrity": "sha1-DS1NSKcYyMBEdp/cT4lZLci2lYU=",
+ "dev": true
+ },
+ "node_modules/bs-snippet-injector": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/bs-snippet-injector/-/bs-snippet-injector-2.0.1.tgz",
+ "integrity": "sha1-YbU5PxH1JVntEgaTEANDtu2wTdU=",
+ "dev": true
+ },
+ "node_modules/buffer": {
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+ "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
"dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "memory-fs": "^0.5.0",
- "tapable": "^1.0.0"
+ "dependencies": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4",
+ "isarray": "^1.0.0"
}
},
- "ent": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
- "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
+ "node_modules/buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
- "entities": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
- "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
+ "node_modules/buffer-indexof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
+ "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
"dev": true
},
- "err-code": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz",
- "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=",
+ "node_modules/buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
"dev": true
},
- "errno": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
- "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
- "dev": true,
- "requires": {
- "prr": "~1.0.1"
- }
- },
- "error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "node_modules/builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
"dev": true,
- "requires": {
- "is-arrayish": "^0.2.1"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "es-abstract": {
- "version": "1.17.4",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
- "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==",
+ "node_modules/builtin-status-codes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+ "dev": true
+ },
+ "node_modules/builtins": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
+ "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=",
+ "dev": true
+ },
+ "node_modules/byline": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
+ "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
"dev": true,
- "requires": {
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-symbols": "^1.0.1",
- "is-callable": "^1.1.5",
- "is-regex": "^1.0.5",
- "object-inspect": "^1.7.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.0",
- "string.prototype.trimleft": "^2.1.1",
- "string.prototype.trimright": "^2.1.1"
+ "engines": {
+ "node": ">= 0.8"
}
},
- "es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "node_modules/cacache": {
+ "version": "15.0.3",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.3.tgz",
+ "integrity": "sha512-bc3jKYjqv7k4pWh7I/ixIjfcjPul4V4jme/WbjvwGS5LzoPL/GzXr4C5EgPNLO/QEZl9Oi61iGitYEdwcrwLCQ==",
"dev": true,
- "requires": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^5.1.1",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "move-file": "^2.0.0",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.0",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 10"
}
},
- "es6-promise": {
- "version": "4.2.6",
- "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz",
- "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==",
- "dev": true
+ "node_modules/cacache/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
},
- "es6-promisify": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
- "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+ "node_modules/cacache/node_modules/tar": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz",
+ "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==",
"dev": true,
- "requires": {
- "es6-promise": "^4.0.3"
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
}
},
- "escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
- "dev": true
+ "node_modules/cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "dev": true,
+ "dependencies": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
- "escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
- "dev": true
+ "node_modules/cacheable-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+ "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "dev": true,
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
- "eslint-scope": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
- "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+ "node_modules/cacheable-request/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
- "requires": {
- "esrecurse": "^4.1.0",
- "estraverse": "^4.1.1"
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "node_modules/cacheable-request/node_modules/http-cache-semantics": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"dev": true
},
- "esrecurse": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
- "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "node_modules/cacheable-request/node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
- "requires": {
- "estraverse": "^4.1.0"
+ "engines": {
+ "node": ">=8"
}
},
- "estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "dev": true
+ "node_modules/cacheable-request/node_modules/normalize-url": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
+ "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
},
- "esutils": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
- "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+ "node_modules/call-me-maybe": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+ "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
"dev": true
},
- "etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
- "dev": true
+ "node_modules/caller-callsite": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
+ "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
},
- "eventemitter3": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
- "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
- "dev": true
+ "node_modules/caller-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
+ "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
+ "dev": true,
+ "dependencies": {
+ "caller-callsite": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
},
- "events": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
- "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
- "dev": true
+ "node_modules/callsite": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+ "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
},
- "eventsource": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
- "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
+ "node_modules/callsites": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
+ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
"dev": true,
- "requires": {
- "original": "^1.0.0"
+ "engines": {
+ "node": ">=4"
}
},
- "evp_bytestokey": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
- "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
- "requires": {
- "md5.js": "^1.3.4",
- "safe-buffer": "^5.1.1"
+ "engines": {
+ "node": ">=6"
}
},
- "execa": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
- "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "node_modules/caniuse-api": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
+ "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
"dev": true,
- "requires": {
- "cross-spawn": "^6.0.0",
- "get-stream": "^4.0.0",
- "is-stream": "^1.1.0",
- "npm-run-path": "^2.0.0",
- "p-finally": "^1.0.0",
- "signal-exit": "^3.0.0",
- "strip-eof": "^1.0.0"
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "caniuse-lite": "^1.0.0",
+ "lodash.memoize": "^4.1.2",
+ "lodash.uniq": "^4.5.0"
}
},
- "exit": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
- "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001116",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz",
+ "integrity": "sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==",
"dev": true
},
- "expand-brackets": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
- "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "node_modules/canonical-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz",
+ "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==",
+ "dev": true
+ },
+ "node_modules/caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "dev": true
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
- "requires": {
- "debug": "^2.3.3",
- "define-property": "^0.2.5",
- "extend-shallow": "^2.0.1",
- "posix-character-classes": "^0.1.0",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.1"
- },
"dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^0.1.0"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- }
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
}
},
- "exports-loader": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.6.3.tgz",
- "integrity": "sha1-V9x4kX9wm5byR/qR5ptVTIVQE8g=",
- "dev": true,
- "requires": {
- "loader-utils": "0.2.x",
- "source-map": "0.1.x"
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "node_modules/chokidar": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
+ "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.1.2",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.4.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.1.2"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
+ "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/chrome-trace-event/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
+ "dev": true
+ },
+ "node_modules/cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/circular-dependency-plugin": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz",
+ "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "dev": true,
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-boxes": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz",
+ "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.4.0.tgz",
+ "integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
+ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
+ "dev": true
+ },
+ "node_modules/cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+ "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
+ "dev": true,
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "node_modules/coa": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
+ "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
+ "dev": true,
+ "dependencies": {
+ "@types/q": "^1.5.1",
+ "chalk": "^2.4.1",
+ "q": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/codelyzer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.0.tgz",
+ "integrity": "sha512-edJIQCIcxD9DhVSyBEdJ38AbLikm515Wl91t5RDGNT88uA6uQdTm4phTWfn9JhzAI8kXNUcfYyAE90lJElpGtA==",
+ "dev": true,
+ "dependencies": {
+ "@angular/compiler": "9.0.0",
+ "@angular/core": "9.0.0",
+ "app-root-path": "^3.0.0",
+ "aria-query": "^3.0.0",
+ "axobject-query": "2.0.2",
+ "css-selector-tokenizer": "^0.7.1",
+ "cssauron": "^1.4.0",
+ "damerau-levenshtein": "^1.0.4",
+ "rxjs": "^6.5.3",
+ "semver-dsl": "^1.0.1",
+ "source-map": "^0.5.7",
+ "sprintf-js": "^1.1.2",
+ "tslib": "^1.10.0",
+ "zone.js": "~0.10.3"
+ }
+ },
+ "node_modules/codelyzer/node_modules/@angular/compiler": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz",
+ "integrity": "sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==",
+ "dev": true
+ },
+ "node_modules/codelyzer/node_modules/@angular/core": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz",
+ "integrity": "sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==",
+ "dev": true
+ },
+ "node_modules/codelyzer/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/codelyzer/node_modules/sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true
+ },
+ "node_modules/codelyzer/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "dev": true,
+ "dependencies": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/color": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz",
+ "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.1",
+ "color-string": "^1.5.2"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/color-string": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
+ "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+ "dev": true
+ },
+ "node_modules/component-bind": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+ "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+ "dev": true
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+ "dev": true
+ },
+ "node_modules/component-inherit": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
+ "dev": true
+ },
+ "node_modules/compose-function": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
+ "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=",
+ "dev": true,
+ "dependencies": {
+ "arity-n": "^1.0.4"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "engines": [
+ "node >= 0.8"
+ ],
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/configstore": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+ "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+ "dev": true,
+ "dependencies": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/configstore/node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/configstore/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/connect": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
+ "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "finalhandler": "1.1.2",
+ "parseurl": "~1.3.3",
+ "utils-merge": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/connect-history-api-fallback": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
+ "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/connect/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/connect/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/console-browserify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
+ "dev": true
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+ "dev": true
+ },
+ "node_modules/constants-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+ "dev": true
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
+ "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-parser": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
+ "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
+ "dependencies": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "node_modules/copy-concurrently": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
+ "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
+ "dev": true,
+ "dependencies": {
+ "aproba": "^1.1.1",
+ "fs-write-stream-atomic": "^1.0.8",
+ "iferr": "^0.1.5",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.4",
+ "run-queue": "^1.0.0"
+ }
+ },
+ "node_modules/copy-concurrently/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.0.3.tgz",
+ "integrity": "sha512-q5m6Vz4elsuyVEIUXr7wJdIdePWTubsqVbEMvf1WQnHGv0Q+9yPRu7MtYFPt+GBOXRav9lvIINifTQ1vSCs+eA==",
+ "dev": true,
+ "dependencies": {
+ "cacache": "^15.0.4",
+ "fast-glob": "^3.2.4",
+ "find-cache-dir": "^3.3.1",
+ "glob-parent": "^5.1.1",
+ "globby": "^11.0.1",
+ "loader-utils": "^2.0.0",
+ "normalize-path": "^3.0.0",
+ "p-limit": "^3.0.1",
+ "schema-utils": "^2.7.0",
+ "serialize-javascript": "^4.0.0",
+ "webpack-sources": "^1.4.3"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/cacache": {
+ "version": "15.0.5",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz",
+ "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/move-file": "^1.0.1",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.0",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/p-limit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz",
+ "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/tar": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz",
+ "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==",
+ "dev": true,
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/core-js": {
+ "version": "2.6.11",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
+ "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz",
+ "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.8.5",
+ "semver": "7.0.0"
+ }
+ },
+ "node_modules/core-js-compat/node_modules/semver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
+ "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+ "dev": true
+ },
+ "node_modules/cosmiconfig": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
+ "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
+ "dev": true,
+ "dependencies": {
+ "import-fresh": "^2.0.0",
+ "is-directory": "^0.3.1",
+ "js-yaml": "^3.13.1",
+ "parse-json": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/create-ecdh": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
+ "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ }
+ },
+ "node_modules/create-ecdh/node_modules/bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ },
+ "node_modules/create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "dev": true,
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "dev": true,
+ "dependencies": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "dev": true,
+ "dependencies": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/css": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
+ "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "source-map": "^0.6.1",
+ "source-map-resolve": "^0.5.2",
+ "urix": "^0.1.0"
+ }
+ },
+ "node_modules/css-color-names": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
+ "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/css-declaration-sorter": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz",
+ "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.1",
+ "timsort": "^0.3.0"
+ },
+ "engines": {
+ "node": ">4"
+ }
+ },
+ "node_modules/css-loader": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.5.3.tgz",
+ "integrity": "sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "cssesc": "^3.0.0",
+ "icss-utils": "^4.1.1",
+ "loader-utils": "^1.2.3",
+ "normalize-path": "^3.0.0",
+ "postcss": "^7.0.27",
+ "postcss-modules-extract-imports": "^2.0.0",
+ "postcss-modules-local-by-default": "^3.0.2",
+ "postcss-modules-scope": "^2.2.0",
+ "postcss-modules-values": "^3.0.0",
+ "postcss-value-parser": "^4.0.3",
+ "schema-utils": "^2.6.6",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/css-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/css-loader/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/css-loader/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/css-parse": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
+ "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
+ "dev": true,
+ "dependencies": {
+ "css": "^2.0.0"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
+ "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
+ "dev": true,
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^3.2.1",
+ "domutils": "^1.7.0",
+ "nth-check": "^1.0.2"
+ }
+ },
+ "node_modules/css-select-base-adapter": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
+ "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
+ "dev": true
+ },
+ "node_modules/css-selector-tokenizer": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz",
+ "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "fastparse": "^1.1.2"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "1.0.0-alpha.37",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
+ "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.0.4",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/css-tree/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.3.0.tgz",
+ "integrity": "sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/css/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/cssauron": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz",
+ "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=",
+ "dev": true,
+ "dependencies": {
+ "through": "X.X.X"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssnano": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
+ "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
+ "dev": true,
+ "dependencies": {
+ "cosmiconfig": "^5.0.0",
+ "cssnano-preset-default": "^4.0.7",
+ "is-resolvable": "^1.0.0",
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/cssnano-preset-default": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz",
+ "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==",
+ "dev": true,
+ "dependencies": {
+ "css-declaration-sorter": "^4.0.1",
+ "cssnano-util-raw-cache": "^4.0.1",
+ "postcss": "^7.0.0",
+ "postcss-calc": "^7.0.1",
+ "postcss-colormin": "^4.0.3",
+ "postcss-convert-values": "^4.0.1",
+ "postcss-discard-comments": "^4.0.2",
+ "postcss-discard-duplicates": "^4.0.2",
+ "postcss-discard-empty": "^4.0.1",
+ "postcss-discard-overridden": "^4.0.1",
+ "postcss-merge-longhand": "^4.0.11",
+ "postcss-merge-rules": "^4.0.3",
+ "postcss-minify-font-values": "^4.0.2",
+ "postcss-minify-gradients": "^4.0.2",
+ "postcss-minify-params": "^4.0.2",
+ "postcss-minify-selectors": "^4.0.2",
+ "postcss-normalize-charset": "^4.0.1",
+ "postcss-normalize-display-values": "^4.0.2",
+ "postcss-normalize-positions": "^4.0.2",
+ "postcss-normalize-repeat-style": "^4.0.2",
+ "postcss-normalize-string": "^4.0.2",
+ "postcss-normalize-timing-functions": "^4.0.2",
+ "postcss-normalize-unicode": "^4.0.1",
+ "postcss-normalize-url": "^4.0.1",
+ "postcss-normalize-whitespace": "^4.0.2",
+ "postcss-ordered-values": "^4.1.2",
+ "postcss-reduce-initial": "^4.0.3",
+ "postcss-reduce-transforms": "^4.0.2",
+ "postcss-svgo": "^4.0.2",
+ "postcss-unique-selectors": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/cssnano-util-get-arguments": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz",
+ "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/cssnano-util-get-match": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz",
+ "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/cssnano-util-raw-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz",
+ "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/cssnano-util-same-parent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz",
+ "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/csso": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz",
+ "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==",
+ "dev": true,
+ "dependencies": {
+ "css-tree": "1.0.0-alpha.39"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/css-tree": {
+ "version": "1.0.0-alpha.39",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz",
+ "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.0.6",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/mdn-data": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz",
+ "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==",
+ "dev": true
+ },
+ "node_modules/csso/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
+ "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==",
+ "dev": true
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ },
+ "node_modules/custom-event": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+ "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
+ "dev": true
+ },
+ "node_modules/cyclist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
+ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
+ "dev": true
+ },
+ "node_modules/d": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+ "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+ "dev": true,
+ "dependencies": {
+ "es5-ext": "^0.10.50",
+ "type": "^1.0.1"
+ }
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
+ "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==",
+ "dev": true
+ },
+ "node_modules/dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "dev": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
+ "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.3",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/date-format": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz",
+ "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/debuglog": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
+ "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
+ "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==",
+ "dev": true
+ },
+ "node_modules/decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+ "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+ "dev": true,
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/deep-equal": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
+ "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
+ "dev": true,
+ "dependencies": {
+ "is-arguments": "^1.0.4",
+ "is-date-object": "^1.0.1",
+ "is-regex": "^1.0.4",
+ "object-is": "^1.0.1",
+ "object-keys": "^1.1.1",
+ "regexp.prototype.flags": "^1.2.0"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "node_modules/default-gateway": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
+ "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==",
+ "dev": true,
+ "dependencies": {
+ "execa": "^1.0.0",
+ "ip-regex": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
+ "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+ "dev": true,
+ "dependencies": {
+ "clone": "^1.0.2"
+ }
+ },
+ "node_modules/defaults/node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
+ "dev": true
+ },
+ "node_modules/deferred-leveldown": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz",
+ "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==",
+ "optional": true,
+ "dependencies": {
+ "abstract-leveldown": "~6.2.1",
+ "inherits": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "dependencies": {
+ "object-keys": "^1.0.12"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/define-property/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/define-property/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/define-property/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
+ "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/glob": "^7.1.1",
+ "globby": "^6.1.0",
+ "is-path-cwd": "^2.0.0",
+ "is-path-in-cwd": "^2.0.0",
+ "p-map": "^2.0.0",
+ "pify": "^4.0.1",
+ "rimraf": "^2.6.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/del/node_modules/array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+ "dev": true,
+ "dependencies": {
+ "array-uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del/node_modules/globby": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+ "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^1.0.1",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del/node_modules/globby/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del/node_modules/p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/del/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+ "dev": true
+ },
+ "node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/dependency-graph": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz",
+ "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/des.js": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
+ "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "node_modules/detect-node": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
+ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
+ "dev": true
+ },
+ "node_modules/dev-ip": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz",
+ "integrity": "sha1-p2o+0YVb56ASu4rBbLgPPADcKPA=",
+ "dev": true,
+ "bin": {
+ "dev-ip": "lib/dev-ip.js"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/dezalgo": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
+ "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
+ "dev": true,
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/di": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+ "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
+ "dev": true
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "node_modules/diffie-hellman/node_modules/bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true
+ },
+ "node_modules/dns-equal": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
+ "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+ "dev": true
+ },
+ "node_modules/dns-packet": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
+ "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+ "dev": true,
+ "dependencies": {
+ "ip": "^1.1.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/dns-txt": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
+ "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+ "dev": true,
+ "dependencies": {
+ "buffer-indexof": "^1.0.0"
+ }
+ },
+ "node_modules/dom-serialize": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+ "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+ "dev": true,
+ "dependencies": {
+ "custom-event": "~1.0.0",
+ "ent": "~2.2.0",
+ "extend": "^3.0.0",
+ "void-elements": "^2.0.0"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+ "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+ "dev": true,
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/dom-serializer/node_modules/domelementtype": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
+ "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
+ "dev": true
+ },
+ "node_modules/domain-browser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4",
+ "npm": ">=1.2"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+ "dev": true
+ },
+ "node_modules/domexception": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
+ "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
+ "dev": true,
+ "dependencies": {
+ "webidl-conversions": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/domino": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
+ "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ=="
+ },
+ "node_modules/domutils": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+ "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+ "dev": true,
+ "dependencies": {
+ "dom-serializer": "0",
+ "domelementtype": "1"
+ }
+ },
+ "node_modules/dot-prop": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
+ "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
+ "dev": true,
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/duplexer3": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
+ "dev": true
+ },
+ "node_modules/duplexify": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "node_modules/easy-extender": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz",
+ "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.10"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/eazy-logger": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-3.1.0.tgz",
+ "integrity": "sha512-/snsn2JqBtUSSstEl4R0RKjkisGHAhvYj89i7r3ytNUKW12y178KDZwXLXIgwDqLW6E/VRMT9qfld7wvFae8bQ==",
+ "dev": true,
+ "dependencies": {
+ "tfunk": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "dev": true,
+ "dependencies": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.3.537",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.537.tgz",
+ "integrity": "sha512-v1jGX46P9vq1XvCBFJ7T7rHd2kMuSrCHnYvO0TqNoURYt7VoxCnqo5+W84s0jlnq0iQUPk5H2D01RfL4ENe2CA==",
+ "dev": true
+ },
+ "node_modules/elliptic": {
+ "version": "6.5.3",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
+ "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.4.0",
+ "brorand": "^1.0.1",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.0"
+ }
+ },
+ "node_modules/elliptic/node_modules/bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/encoding-down": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz",
+ "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==",
+ "optional": true,
+ "dependencies": {
+ "abstract-leveldown": "^6.2.1",
+ "inherits": "^2.0.3",
+ "level-codec": "^9.0.0",
+ "level-errors": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/encoding/node_modules/iconv-lite": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
+ "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz",
+ "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "0.3.1",
+ "debug": "~4.1.0",
+ "engine.io-parser": "~2.2.0",
+ "ws": "^7.1.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/engine.io-client": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz",
+ "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==",
+ "dev": true,
+ "dependencies": {
+ "component-emitter": "~1.3.0",
+ "component-inherit": "0.0.3",
+ "debug": "~4.1.0",
+ "engine.io-parser": "~2.2.0",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "ws": "~6.1.0",
+ "xmlhttprequest-ssl": "~1.5.4",
+ "yeast": "0.1.2"
+ }
+ },
+ "node_modules/engine.io-client/node_modules/ws": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
+ "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
+ "dev": true,
+ "dependencies": {
+ "async-limiter": "~1.0.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz",
+ "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==",
+ "dev": true,
+ "dependencies": {
+ "after": "0.8.2",
+ "arraybuffer.slice": "~0.0.7",
+ "base64-arraybuffer": "0.1.5",
+ "blob": "0.0.5",
+ "has-binary2": "~1.0.2"
+ }
+ },
+ "node_modules/engine.io/node_modules/cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/ws": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
+ "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.3.0"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz",
+ "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "memory-fs": "^0.5.0",
+ "tapable": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/ent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+ "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
+ "dev": true
+ },
+ "node_modules/entities": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
+ "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
+ "dev": true
+ },
+ "node_modules/err-code": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz",
+ "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=",
+ "dev": true
+ },
+ "node_modules/errno": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+ "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+ "devOptional": true,
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/error-stack-parser": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
+ "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
+ "dependencies": {
+ "stackframe": "^1.1.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.17.6",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
+ "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
+ "dev": true,
+ "dependencies": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.0",
+ "is-regex": "^1.1.0",
+ "object-inspect": "^1.7.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.0",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es5-ext": {
+ "version": "0.10.53",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
+ "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
+ "dev": true,
+ "dependencies": {
+ "es6-iterator": "~2.0.3",
+ "es6-symbol": "~3.1.3",
+ "next-tick": "~1.0.0"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+ "dev": true,
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "dev": true
+ },
+ "node_modules/es6-promisify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+ "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+ "dev": true,
+ "dependencies": {
+ "es6-promise": "^4.0.3"
+ }
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+ "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+ "dev": true,
+ "dependencies": {
+ "d": "^1.0.1",
+ "ext": "^1.1.2"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz",
+ "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-goat": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
+ "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
+ "dev": true,
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+ "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ev-emitter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz",
+ "integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q=="
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
+ "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==",
+ "dev": true
+ },
+ "node_modules/events": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
+ "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
+ "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
+ "dev": true,
+ "dependencies": {
+ "original": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "dev": true,
+ "dependencies": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "dev": true,
+ "dependencies": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/exports-loader": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.6.3.tgz",
+ "integrity": "sha1-V9x4kX9wm5byR/qR5ptVTIVQE8g=",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "0.2.x",
+ "source-map": "0.1.x"
+ }
+ },
+ "node_modules/exports-loader/node_modules/big.js": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/exports-loader/node_modules/emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/exports-loader/node_modules/json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/exports-loader/node_modules/loader-utils": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+ "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^3.1.3",
+ "emojis-list": "^2.0.0",
+ "json5": "^0.5.0",
+ "object-assign": "^4.0.1"
+ }
+ },
+ "node_modules/exports-loader/node_modules/source-map": {
+ "version": "0.1.43",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+ "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+ "dev": true,
+ "dependencies": {
+ "amdefine": ">=0.0.4"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "dependencies": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/express/node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "node_modules/ext": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
+ "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
+ "dev": true,
+ "dependencies": {
+ "type": "^2.0.0"
+ }
+ },
+ "node_modules/ext/node_modules/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==",
+ "dev": true
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extend-shallow/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/external-editor/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "dev": true,
+ "dependencies": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ]
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz",
+ "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.0",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.2",
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "node_modules/fastparse": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
+ "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz",
+ "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+ "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+ "dev": true,
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/figgy-pudding": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
+ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==",
+ "dev": true
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/file-loader": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.0.0.tgz",
+ "integrity": "sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.5"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "node_modules/find-cache-dir": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
+ "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==",
+ "dev": true,
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
+ "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
+ "dev": true
+ },
+ "node_modules/flush-write-stream": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.3.6"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
+ "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "dev": true,
+ "dependencies": {
+ "map-cache": "^0.2.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/from2": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+ "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz",
+ "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-write-stream-atomic": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
+ "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "iferr": "^0.1.5",
+ "imurmurhash": "^0.1.4",
+ "readable-stream": "1 || 2"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/fuse.js": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-5.2.3.tgz",
+ "integrity": "sha512-ld3AEgKtKnnXCtJavtygAb+aLlD5aVvLwTocXXBSStLA6JGFI6oMxTvumwh46N2/3gs3A7JNDu1px5F1/cq84g==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/gauge": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "dev": true,
+ "dependencies": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "node_modules/gauge/node_modules/is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gauge/node_modules/string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/genfun": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz",
+ "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==",
+ "dev": true
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.1",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz",
+ "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "dev": true,
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-value": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "dev": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz",
+ "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
+ "dev": true
+ },
+ "node_modules/global-dirs": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz",
+ "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==",
+ "dev": true,
+ "dependencies": {
+ "ini": "^1.3.5"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz",
+ "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.1.1",
+ "ignore": "^5.1.4",
+ "merge2": "^1.3.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/google-closure-library": {
+ "version": "20190325.0.0",
+ "resolved": "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20190325.0.0.tgz",
+ "integrity": "sha512-B+Cdh2c3BbvSIONufK3yU/yKwhm7vxaqrAvxIBo3JmUAhA3WQPRSculbJPKC4ca7b/pjlsIR76KDpVqVrJd4dg==",
+ "dev": true
+ },
+ "node_modules/got": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+ "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+ "dev": true,
+ "dependencies": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
+ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
+ "dev": true
+ },
+ "node_modules/guess-parser": {
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/guess-parser/-/guess-parser-0.4.21.tgz",
+ "integrity": "sha512-DDrCBOx1g4KvamxwlLPA4bMdJWXEDSnRIFIUIllIhZ4hy2eOTQtn1DyVak7uUtsN9Zp11JUFNdDEnChjkRmFxg==",
+ "dev": true,
+ "dependencies": {
+ "@wessberg/ts-evaluator": "0.0.26"
+ }
+ },
+ "node_modules/handle-thing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+ "dev": true
+ },
+ "node_modules/har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/har-validator": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
+ "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.3",
+ "har-schema": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-binary2": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+ "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+ "dev": true,
+ "dependencies": {
+ "isarray": "2.0.1"
+ }
+ },
+ "node_modules/has-binary2/node_modules/isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+ "dev": true
+ },
+ "node_modules/has-cors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+ "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=",
+ "dev": true
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
+ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+ "dev": true
+ },
+ "node_modules/has-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+ "dev": true,
+ "dependencies": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/kind-of": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-yarn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
+ "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hash-base": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
+ "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/hash-base/node_modules/readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/hash-base/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "node_modules/hash.js": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "node_modules/hex-color-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
+ "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==",
+ "dev": true
+ },
+ "node_modules/hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+ "dev": true,
+ "dependencies": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.5.tgz",
+ "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/hpack.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+ "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "obuf": "^1.0.0",
+ "readable-stream": "^2.0.1",
+ "wbuf": "^1.1.0"
+ }
+ },
+ "node_modules/hsl-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz",
+ "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=",
+ "dev": true
+ },
+ "node_modules/hsla-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz",
+ "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
+ "dev": true
+ },
+ "node_modules/html-comment-regex": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
+ "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
+ "dev": true
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
+ "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-encoding": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/html-entities": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz",
+ "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==",
+ "dev": true
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz",
+ "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==",
+ "dev": true
+ },
+ "node_modules/http-deceiver": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+ "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
+ "dev": true
+ },
+ "node_modules/http-errors": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/http-errors/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
+ "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "4",
+ "debug": "3.1.0"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/http-proxy-middleware": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
+ "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+ "dev": true,
+ "dependencies": {
+ "http-proxy": "^1.17.0",
+ "is-glob": "^4.0.0",
+ "lodash": "^4.17.11",
+ "micromatch": "^3.1.10"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/braces/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/fill-range/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-proxy-middleware/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "dev": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.8",
+ "npm": ">=1.3.7"
+ }
+ },
+ "node_modules/https-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+ "dev": true
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
+ "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^4.3.0",
+ "debug": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/huebee": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/huebee/-/huebee-2.1.0.tgz",
+ "integrity": "sha512-2im03Zw7MosL/h389ZwyMFv71JTglM4XvoahPRApajVthqBDS9Ro00zgTv6VKW5AXwZ83pNMDhCXC4TMluCSlg==",
+ "dependencies": {
+ "ev-emitter": "^1.1.1",
+ "unipointer": "^2.3.0"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz",
+ "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
+ "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.14"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
+ "devOptional": true
+ },
+ "node_modules/iferr": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
+ "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
+ "dev": true
+ },
+ "node_modules/ignore": {
+ "version": "5.1.8",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
+ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=",
+ "dev": true
+ },
+ "node_modules/ignore-walk": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
+ "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
+ "dev": true,
+ "dependencies": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "node_modules/image-size": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+ "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
+ "dev": true,
+ "optional": true,
+ "bin": {
+ "image-size": "bin/image-size.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
+ "dev": true
+ },
+ "node_modules/immutable": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+ "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-cwd": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
+ "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=",
+ "dev": true,
+ "dependencies": {
+ "import-from": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
+ "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
+ "dev": true,
+ "dependencies": {
+ "caller-path": "^2.0.0",
+ "resolve-from": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-from": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz",
+ "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=",
+ "dev": true,
+ "dependencies": {
+ "resolve-from": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-lazy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+ "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",
+ "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==",
+ "dev": true,
+ "dependencies": {
+ "pkg-dir": "^3.0.0",
+ "resolve-cwd": "^2.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/imports-loader": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-0.6.5.tgz",
+ "integrity": "sha1-rnRlMDHVnjezwvslRKxhrq41MKY=",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "0.2.x",
+ "source-map": "0.1.x"
+ }
+ },
+ "node_modules/imports-loader/node_modules/big.js": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/imports-loader/node_modules/emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/imports-loader/node_modules/json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/imports-loader/node_modules/loader-utils": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+ "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^3.1.3",
+ "emojis-list": "^2.0.0",
+ "json5": "^0.5.0",
+ "object-assign": "^4.0.1"
+ }
+ },
+ "node_modules/imports-loader/node_modules/source-map": {
+ "version": "0.1.43",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+ "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+ "dev": true,
+ "dependencies": {
+ "amdefine": ">=0.0.4"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/indexes-of": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
+ "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+ "dev": true
+ },
+ "node_modules/indexof": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+ "dev": true
+ },
+ "node_modules/infer-owner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
+ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
+ "dev": true
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "devOptional": true
+ },
+ "node_modules/ini": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/inquirer": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
+ "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^3.0.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.15",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.5.3",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/inquirer/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/inquirer/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/internal-ip": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz",
+ "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==",
+ "dev": true,
+ "dependencies": {
+ "default-gateway": "^4.2.0",
+ "ipaddr.js": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "dev": true,
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "node_modules/invert-kv": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ip": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+ "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
+ "dev": true
+ },
+ "node_modules/ip-regex": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
+ "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-absolute-url": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz",
+ "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-accessor-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-accessor-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
+ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
+ "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+ "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+ "dev": true,
+ "dependencies": {
+ "ci-info": "^2.0.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-color-stop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
+ "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=",
+ "dev": true,
+ "dependencies": {
+ "css-color-names": "^0.0.4",
+ "hex-color-regex": "^1.1.0",
+ "hsl-regex": "^1.0.0",
+ "hsla-regex": "^1.0.0",
+ "rgb-regex": "^1.0.1",
+ "rgba-regex": "^1.0.0"
+ }
+ },
+ "node_modules/is-data-descriptor": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-data-descriptor/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
+ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-descriptor/node_modules/kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-directory": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+ "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz",
+ "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-installed-globally": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz",
+ "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==",
+ "dev": true,
+ "dependencies": {
+ "global-dirs": "^2.0.1",
+ "is-path-inside": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-installed-globally/node_modules/is-path-inside": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
+ "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-npm": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz",
+ "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-like": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz",
+ "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==",
+ "dev": true,
+ "dependencies": {
+ "lodash.isfinite": "^3.3.2"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-path-cwd": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+ "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-path-in-cwd": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+ "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
+ "dev": true,
+ "dependencies": {
+ "is-path-inside": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+ "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
+ "dev": true,
+ "dependencies": {
+ "path-is-inside": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
+ "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=",
+ "dev": true
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+ "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-resolvable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
+ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
+ "dev": true
+ },
+ "node_modules/is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-svg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz",
+ "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==",
+ "dev": true,
+ "dependencies": {
+ "html-comment-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+ "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+ "dev": true
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-yarn-global": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
+ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
+ "dev": true
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
+ "node_modules/isbinaryfile": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz",
+ "integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/iserror": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/iserror/-/iserror-0.0.2.tgz",
+ "integrity": "sha1-vVNFH+L2aLnyQCwZZnh6qix8C/U="
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isomorphic.js": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.5.tgz",
+ "integrity": "sha512-MkX5lLQApx/8IAIU31PKvpAZosnu2Jqcj1rM8TzxyA4CR96tv3SgMKQNTCxL58G7696Q57zd7ubHV/hTg+5fNA=="
+ },
+ "node_modules/isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+ "dev": true
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz",
+ "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz",
+ "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.7.5",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.0.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^3.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz",
+ "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^2.0.5",
+ "make-dir": "^2.1.0",
+ "rimraf": "^2.6.3",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
+ "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz",
+ "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==",
+ "dev": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jasmine": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.1.tgz",
+ "integrity": "sha512-Jqp8P6ZWkTVFGmJwBK46p+kJNrZCdqkQ4GL+PGuBXZwK1fM4ST9BizkYgIwCFqYYqnTizAy6+XG2Ej5dFrej9Q==",
+ "dev": true,
+ "dependencies": {
+ "fast-glob": "^2.2.6",
+ "jasmine-core": "~3.6.0"
+ },
+ "bin": {
+ "jasmine": "bin/jasmine.js"
+ }
+ },
+ "node_modules/jasmine-core": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
+ "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
+ "dev": true
+ },
+ "node_modules/jasmine-spec-reporter": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.2.tgz",
+ "integrity": "sha512-6gP1LbVgJ+d7PKksQBc2H0oDGNRQI3gKUsWlswKaQ2fif9X5gzhQcgM5+kiJGCQVurOG09jqNhk7payggyp5+g==",
+ "dev": true,
+ "dependencies": {
+ "colors": "1.4.0"
+ }
+ },
+ "node_modules/jasmine-ts": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/jasmine-ts/-/jasmine-ts-0.3.0.tgz",
+ "integrity": "sha512-K5joodjVOh3bnD06CNXC8P5htDq/r0Rhjv66ECOpdIGFLly8kM7V+X/GXcd9kv+xO+tIq3q9Y8B5OF6yr/iiDw==",
+ "dev": true,
+ "dependencies": {
+ "yargs": "^8.0.2"
+ },
+ "bin": {
+ "jasmine-ts": "lib/index.js"
+ },
+ "engines": {
+ "node": ">= 5.12"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/camelcase": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/cliui": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+ "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/cliui/node_modules/string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "dev": true
+ },
+ "node_modules/jasmine-ts/node_modules/is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+ "dev": true
+ },
+ "node_modules/jasmine-ts/node_modules/string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "dependencies": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/string-width/node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/string-width/node_modules/strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/y18n": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+ "dev": true
+ },
+ "node_modules/jasmine-ts/node_modules/yargs": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz",
+ "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^4.1.0",
+ "cliui": "^3.2.0",
+ "decamelize": "^1.1.1",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^2.0.0",
+ "read-pkg-up": "^2.0.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^7.0.0"
+ }
+ },
+ "node_modules/jasmine-ts/node_modules/yargs-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz",
+ "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^4.1.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/@nodelib/fs.stat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
+ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/jasmine/node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/braces/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/fast-glob": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
+ "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==",
+ "dev": true,
+ "dependencies": {
+ "@mrmlnc/readdir-enhanced": "^2.2.1",
+ "@nodelib/fs.stat": "^1.1.2",
+ "glob-parent": "^3.1.0",
+ "is-glob": "^4.0.0",
+ "merge2": "^1.2.3",
+ "micromatch": "^3.1.10"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/fill-range/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/glob-parent/node_modules/is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/jasmine-core": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz",
+ "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==",
+ "dev": true
+ },
+ "node_modules/jasmine/node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasmine/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jasminewd2": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz",
+ "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
+ "dev": true,
+ "engines": {
+ "node": ">= 6.9.x"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "26.0.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz",
+ "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==",
+ "dev": true,
+ "dependencies": {
+ "merge-stream": "^2.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.14.2"
+ }
+ },
+ "node_modules/jest-worker/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "dev": true
+ },
+ "node_modules/jsdom": {
+ "version": "16.4.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz",
+ "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.3",
+ "acorn": "^7.1.1",
+ "acorn-globals": "^6.0.0",
+ "cssom": "^0.4.4",
+ "cssstyle": "^2.2.0",
+ "data-urls": "^2.0.0",
+ "decimal.js": "^10.2.0",
+ "domexception": "^2.0.1",
+ "escodegen": "^1.14.1",
+ "html-encoding-sniffer": "^2.0.1",
+ "is-potential-custom-element-name": "^1.0.0",
+ "nwsapi": "^2.2.0",
+ "parse5": "5.1.1",
+ "request": "^2.88.2",
+ "request-promise-native": "^1.0.8",
+ "saxes": "^5.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^3.0.1",
+ "w3c-hr-time": "^1.0.2",
+ "w3c-xmlserializer": "^2.0.0",
+ "webidl-conversions": "^6.1.0",
+ "whatwg-encoding": "^1.0.5",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.0.0",
+ "ws": "^7.2.3",
+ "xml-name-validator": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsdom/node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/tough-cookie": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
+ "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
+ "dev": true,
+ "dependencies": {
+ "ip-regex": "^2.1.0",
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
+ "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.4"
+ }
+ },
+ "node_modules/jsdom/node_modules/ws": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
+ "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.3.0"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
+ "dev": true
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
+ "node_modules/json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+ "dev": true
+ },
+ "node_modules/json3": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
+ "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
+ "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
+ "dev": true,
+ "engines": [
+ "node >= 0.2.0"
+ ]
+ },
+ "node_modules/JSONStream": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
+ "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+ "dev": true,
+ "dependencies": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ },
+ "bin": {
+ "JSONStream": "bin.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "dependencies": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "node_modules/jstz": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/jstz/-/jstz-2.1.1.tgz",
+ "integrity": "sha512-8hfl5RD6P7rEeIbzStBz3h4f+BQHfq/ABtoU6gXKQv5OcZhnmrIpG7e1pYaZ8hS9e0mp+bxUj08fnDUbKctYyA==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/jszip": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
+ "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
+ "dev": true,
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "set-immediate-shim": "~1.0.1"
+ }
+ },
+ "node_modules/karma": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/karma/-/karma-5.0.9.tgz",
+ "integrity": "sha512-dUA5z7Lo7G4FRSe1ZAXqOINEEWxmCjDBbfRBmU/wYlSMwxUQJP/tEEP90yJt3Uqo03s9rCgVnxtlfq+uDhxSPg==",
+ "dev": true,
+ "dependencies": {
+ "body-parser": "^1.19.0",
+ "braces": "^3.0.2",
+ "chokidar": "^3.0.0",
+ "colors": "^1.4.0",
+ "connect": "^3.7.0",
+ "di": "^0.0.1",
+ "dom-serialize": "^2.2.1",
+ "flatted": "^2.0.2",
+ "glob": "^7.1.6",
+ "graceful-fs": "^4.2.4",
+ "http-proxy": "^1.18.1",
+ "isbinaryfile": "^4.0.6",
+ "lodash": "^4.17.15",
+ "log4js": "^6.2.1",
+ "mime": "^2.4.5",
+ "minimatch": "^3.0.4",
+ "qjobs": "^1.2.0",
+ "range-parser": "^1.2.1",
+ "rimraf": "^3.0.2",
+ "socket.io": "^2.3.0",
+ "source-map": "^0.6.1",
+ "tmp": "0.2.1",
+ "ua-parser-js": "0.7.21",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "karma": "bin/karma"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/karma-chrome-launcher": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz",
+ "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==",
+ "dev": true,
+ "dependencies": {
+ "which": "^1.2.1"
+ }
+ },
+ "node_modules/karma-cli": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-2.0.0.tgz",
+ "integrity": "sha512-1Kb28UILg1ZsfqQmeELbPzuEb5C6GZJfVIk0qOr8LNYQuYWmAaqP16WpbpKEjhejDrDYyYOwwJXSZO6u7q5Pvw==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.3.3"
+ },
+ "bin": {
+ "karma": "bin/karma"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/karma-coverage-istanbul-reporter": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz",
+ "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^3.0.6",
+ "istanbul-reports": "^3.0.2",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "node_modules/karma-jasmine": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.3.1.tgz",
+ "integrity": "sha512-Nxh7eX9mOQMyK0VSsMxdod+bcqrR/ikrmEiWj5M6fwuQ7oI+YEF1FckaDsWfs6TIpULm9f0fTKMjF7XcrvWyqQ==",
+ "dev": true,
+ "dependencies": {
+ "jasmine-core": "^3.5.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/karma-jasmine-html-reporter": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.5.4.tgz",
+ "integrity": "sha512-PtilRLno5O6wH3lDihRnz0Ba8oSn0YUJqKjjux1peoYGwo0AQqrWRbdWk/RLzcGlb+onTyXAnHl6M+Hu3UxG/Q==",
+ "dev": true
+ },
+ "node_modules/karma-source-map-support": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz",
+ "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==",
+ "dev": true,
+ "dependencies": {
+ "source-map-support": "^0.5.5"
+ }
+ },
+ "node_modules/karma/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/karma/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/karma/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/karma/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/karma/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/mime": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
+ "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/karma/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/karma/node_modules/string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/tmp": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+ "dev": true,
+ "dependencies": {
+ "rimraf": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.17.0"
+ }
+ },
+ "node_modules/karma/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+ "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.0"
+ }
+ },
+ "node_modules/killable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
+ "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==",
+ "dev": true
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/latest-version": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+ "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
+ "dev": true,
+ "dependencies": {
+ "package-json": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lcid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+ "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+ "dev": true,
+ "dependencies": {
+ "invert-kv": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/less": {
+ "version": "3.12.2",
+ "resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
+ "integrity": "sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q==",
+ "dev": true,
+ "dependencies": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "make-dir": "^2.1.0",
+ "mime": "^1.4.1",
+ "native-request": "^1.0.5",
+ "source-map": "~0.6.0",
+ "tslib": "^1.10.0"
+ },
+ "bin": {
+ "lessc": "bin/lessc"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "optionalDependencies": {
+ "image-size": "~0.5.0",
+ "native-request": "^1.0.5",
+ "source-map": "~0.6.0"
+ }
+ },
+ "node_modules/less-loader": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-6.1.0.tgz",
+ "integrity": "sha512-/jLzOwLyqJ7Kt3xg5sHHkXtOyShWwFj410K9Si9WO+/h8rmYxxkSR0A3/hFEntWudE20zZnWMtpMYnLzqTVdUA==",
+ "dev": true,
+ "dependencies": {
+ "clone": "^2.1.2",
+ "less": "^3.11.1",
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.6"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/less/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/less/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/level": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz",
+ "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==",
+ "optional": true,
+ "dependencies": {
+ "level-js": "^5.0.0",
+ "level-packager": "^5.1.0",
+ "leveldown": "^5.4.0"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/level"
+ }
+ },
+ "node_modules/level-codec": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz",
+ "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==",
+ "optional": true,
+ "dependencies": {
+ "buffer": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/level-codec/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "optional": true,
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/level-concat-iterator": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz",
+ "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/level-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz",
+ "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==",
+ "optional": true,
+ "dependencies": {
+ "errno": "~0.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/level-iterator-stream": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz",
+ "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==",
+ "optional": true,
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0",
+ "xtend": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/level-iterator-stream/node_modules/readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "optional": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/level-js": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz",
+ "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==",
+ "optional": true,
+ "dependencies": {
+ "abstract-leveldown": "~6.2.3",
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.3",
+ "ltgt": "^2.1.2"
+ }
+ },
+ "node_modules/level-js/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "optional": true,
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/level-packager": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz",
+ "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==",
+ "optional": true,
+ "dependencies": {
+ "encoding-down": "^6.3.0",
+ "levelup": "^4.3.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/level-supports": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz",
+ "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==",
+ "optional": true,
+ "dependencies": {
+ "xtend": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/leveldown": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz",
+ "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "abstract-leveldown": "~6.2.1",
+ "napi-macros": "~2.0.0",
+ "node-gyp-build": "~4.1.0"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/levelup": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz",
+ "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==",
+ "optional": true,
+ "dependencies": {
+ "deferred-leveldown": "~5.3.0",
+ "level-errors": "~2.0.0",
+ "level-iterator-stream": "~4.0.0",
+ "level-supports": "~1.0.0",
+ "xtend": "~4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levenary": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz",
+ "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==",
+ "dev": true,
+ "dependencies": {
+ "leven": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lib0": {
+ "version": "0.2.35",
+ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.35.tgz",
+ "integrity": "sha512-drVD3EscB3TIxiFzceuZg7oF5Z6I8a0KX+7FowNcAXOEsTej/hlHB+ElJ8Pa/Ge73Gy3fklSJtPxpNd2PajdWg==",
+ "dependencies": {
+ "isomorphic.js": "^0.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/license-webpack-plugin": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.2.0.tgz",
+ "integrity": "sha512-XPsdL/0brSHf+7dXIlRqotnCQ58RX2au6otkOg4U3dm8uH+Ka/fW4iukEs95uXm+qKe/SBs+s1Ll/aQddKG+tg==",
+ "dev": true,
+ "dependencies": {
+ "@types/webpack-sources": "^0.1.5",
+ "webpack-sources": "^1.2.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dev": true,
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/limiter": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==",
+ "dev": true
+ },
+ "node_modules/load-json-file": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+ "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/load-json-file/node_modules/parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "dev": true,
+ "dependencies": {
+ "error-ex": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/load-json-file/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
+ "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.3.0 <5.0.0 || >=5.10"
+ }
+ },
+ "node_modules/loader-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
+ "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/localtunnel": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.0.tgz",
+ "integrity": "sha512-g6E0aLgYYDvQDxIjIXkgJo2+pHj3sGg4Wz/XP3h2KtZnRsWPbOQY+hw1H8Z91jep998fkcVE9l+kghO+97vllg==",
+ "dev": true,
+ "dependencies": {
+ "axios": "0.19.0",
+ "debug": "4.1.1",
+ "openurl": "1.1.1",
+ "yargs": "13.3.0"
+ },
+ "bin": {
+ "lt": "bin/lt.js"
+ },
+ "engines": {
+ "node": ">=8.3.0"
+ }
+ },
+ "node_modules/localtunnel/node_modules/yargs": {
+ "version": "13.3.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
+ "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.1"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.20",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+ "dev": true
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+ "dev": true
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
+ },
+ "node_modules/lodash.isfinite": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz",
+ "integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=",
+ "dev": true
+ },
+ "node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
+ "dev": true
+ },
+ "node_modules/lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
+ "dev": true
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
+ "dev": true
+ },
+ "node_modules/log-symbols": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+ "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log4js": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz",
+ "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==",
+ "dev": true,
+ "dependencies": {
+ "date-format": "^3.0.0",
+ "debug": "^4.1.1",
+ "flatted": "^2.0.1",
+ "rfdc": "^1.1.4",
+ "streamroller": "^2.2.4"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/loglevel": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz",
+ "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lru-cache/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/ltgt": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz",
+ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=",
+ "optional": true
+ },
+ "node_modules/magic-string": {
+ "version": "0.25.7",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+ "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+ "dev": true,
+ "dependencies": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz",
+ "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==",
+ "dev": true,
+ "dependencies": {
+ "agentkeepalive": "^3.4.1",
+ "cacache": "^12.0.0",
+ "http-cache-semantics": "^3.8.1",
+ "http-proxy-agent": "^2.1.0",
+ "https-proxy-agent": "^2.2.3",
+ "lru-cache": "^5.1.1",
+ "mississippi": "^3.0.0",
+ "node-fetch-npm": "^2.0.2",
+ "promise-retry": "^1.1.1",
+ "socks-proxy-agent": "^4.0.0",
+ "ssri": "^6.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/cacache": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz",
+ "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==",
+ "dev": true,
+ "dependencies": {
+ "bluebird": "^3.5.5",
+ "chownr": "^1.1.1",
+ "figgy-pudding": "^3.5.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.1.15",
+ "infer-owner": "^1.0.3",
+ "lru-cache": "^5.1.1",
+ "mississippi": "^3.0.0",
+ "mkdirp": "^0.5.1",
+ "move-concurrently": "^1.0.1",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^2.6.3",
+ "ssri": "^6.0.1",
+ "unique-filename": "^1.1.1",
+ "y18n": "^4.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true
+ },
+ "node_modules/make-fetch-happen/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/ssri": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
+ "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
+ "dev": true,
+ "dependencies": {
+ "figgy-pudding": "^3.5.1"
+ }
+ },
+ "node_modules/map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+ "dev": true,
+ "dependencies": {
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "dev": true,
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
+ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
+ "dev": true
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mem": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
+ "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mem/node_modules/mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/memory-fs": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
+ "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==",
+ "dev": true,
+ "dependencies": {
+ "errno": "^0.1.3",
+ "readable-stream": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4.3.0 <5.0.0 || >=5.10"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "node_modules/merge-source-map": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
+ "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==",
+ "dev": true,
+ "dependencies": {
+ "source-map": "^0.6.1"
+ }
+ },
+ "node_modules/merge-source-map/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "bin": {
+ "miller-rabin": "bin/miller-rabin"
+ }
+ },
+ "node_modules/miller-rabin/node_modules/bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
+ "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.27",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
+ "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
+ "dependencies": {
+ "mime-db": "1.44.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mini-css-extract-plugin": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz",
+ "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^1.1.0",
+ "normalize-url": "1.9.1",
+ "schema-utils": "^1.0.0",
+ "webpack-sources": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 6.9.0"
+ }
+ },
+ "node_modules/mini-css-extract-plugin/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/mini-css-extract-plugin/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mini-css-extract-plugin/node_modules/normalize-url": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
+ "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=",
+ "dev": true,
+ "dependencies": {
+ "object-assign": "^4.0.1",
+ "prepend-http": "^1.0.0",
+ "query-string": "^4.1.0",
+ "sort-keys": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mini-css-extract-plugin/node_modules/schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true
+ },
+ "node_modules/minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+ "dev": true
+ },
+ "node_modules/minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ },
+ "node_modules/minipass": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
+ "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/mississippi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
+ "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
+ "dev": true,
+ "dependencies": {
+ "concat-stream": "^1.5.0",
+ "duplexify": "^3.4.2",
+ "end-of-stream": "^1.1.0",
+ "flush-write-stream": "^1.0.0",
+ "from2": "^2.1.0",
+ "parallel-transform": "^1.1.0",
+ "pump": "^3.0.0",
+ "pumpify": "^1.3.3",
+ "stream-each": "^1.1.0",
+ "through2": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz",
+ "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==",
+ "dev": true
+ },
+ "node_modules/mixin-deep": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+ "dev": true,
+ "dependencies": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mixin-deep/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/move-concurrently": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
+ "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
+ "dev": true,
+ "dependencies": {
+ "aproba": "^1.1.1",
+ "copy-concurrently": "^1.0.0",
+ "fs-write-stream-atomic": "^1.0.8",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.4",
+ "run-queue": "^1.0.3"
+ }
+ },
+ "node_modules/move-concurrently/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/move-file": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/move-file/-/move-file-2.0.0.tgz",
+ "integrity": "sha512-cdkdhNCgbP5dvS4tlGxZbD+nloio9GIimP57EjqFhwLcMjnU+XJKAZzlmg/TN/AK1LuNAdTSvm3CPPP4Xkv0iQ==",
+ "dev": true,
+ "dependencies": {
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10.17"
+ }
+ },
+ "node_modules/move-file/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/multicast-dns": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
+ "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==",
+ "dev": true,
+ "dependencies": {
+ "dns-packet": "^1.3.1",
+ "thunky": "^1.0.2"
+ },
+ "bin": {
+ "multicast-dns": "cli.js"
+ }
+ },
+ "node_modules/multicast-dns-service-types": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
+ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
+ "dev": true
+ },
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "node_modules/nanomatch": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/napi-macros": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz",
+ "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==",
+ "optional": true
+ },
+ "node_modules/native-request": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.7.tgz",
+ "integrity": "sha512-9nRjinI9bmz+S7dgNtf4A70+/vPhnd+2krGpy4SUlADuOuSa24IDkNaZ+R/QT1wQ6S8jBdi6wE7fLekFZNfUpQ==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true
+ },
+ "node_modules/next-tick": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
+ "dev": true
+ },
+ "node_modules/ngx-bootstrap": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-5.6.1.tgz",
+ "integrity": "sha512-8fDs3VaaWgKpupakPKS0QaUc+1E/JMBGJDxUUODjyIkLtFr1A8vH4cjXiV3AfrPvhK27GH0oyTPyKWKcCjEtVg=="
+ },
+ "node_modules/ngx-toastr": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-13.2.0.tgz",
+ "integrity": "sha512-XU+wACX5hxwOJ4BtPMAUExQmYbjfvH3C/R4vcC9QK/dX2Zw+2w9tS9m4W6TUFyR92xZ/tGLBtsqRdrDRn3fJCw==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/common": ">=10.0.0-0",
+ "@angular/core": ">=10.0.0-0",
+ "@angular/platform-browser": ">=10.0.0-0"
+ }
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "node_modules/node-fetch-npm": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz",
+ "integrity": "sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==",
+ "dev": true,
+ "dependencies": {
+ "encoding": "^0.1.11",
+ "json-parse-better-errors": "^1.0.0",
+ "safe-buffer": "^5.1.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/node-forge": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
+ "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-4.0.0.tgz",
+ "integrity": "sha512-2XiryJ8sICNo6ej8d0idXDEMKfVfFK7kekGCtJAuelGsYHQxhj13KTf95swTCN2dZ/4lTfZ84Fu31jqJEEgjWA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.0.3",
+ "graceful-fs": "^4.1.2",
+ "mkdirp": "^0.5.0",
+ "nopt": "2 || 3",
+ "npmlog": "0 || 1 || 2 || 3 || 4",
+ "osenv": "0",
+ "request": "^2.87.0",
+ "rimraf": "2",
+ "semver": "~5.3.0",
+ "tar": "^4.4.8",
+ "which": "1"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz",
+ "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==",
+ "optional": true,
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/node-gyp/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/node-gyp/node_modules/semver": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/node-libs-browser": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
+ "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==",
+ "dev": true,
+ "dependencies": {
+ "assert": "^1.1.1",
+ "browserify-zlib": "^0.2.0",
+ "buffer": "^4.3.0",
+ "console-browserify": "^1.1.0",
+ "constants-browserify": "^1.0.0",
+ "crypto-browserify": "^3.11.0",
+ "domain-browser": "^1.1.1",
+ "events": "^3.0.0",
+ "https-browserify": "^1.0.0",
+ "os-browserify": "^0.3.0",
+ "path-browserify": "0.0.1",
+ "process": "^0.11.10",
+ "punycode": "^1.2.4",
+ "querystring-es3": "^0.2.0",
+ "readable-stream": "^2.3.3",
+ "stream-browserify": "^2.0.1",
+ "stream-http": "^2.7.2",
+ "string_decoder": "^1.0.0",
+ "timers-browserify": "^2.0.4",
+ "tty-browserify": "0.0.0",
+ "url": "^0.11.0",
+ "util": "^0.11.0",
+ "vm-browserify": "^1.0.1"
+ }
+ },
+ "node_modules/node-libs-browser/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "node_modules/node-libs-browser/node_modules/punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+ "dev": true
+ },
+ "node_modules/node-libs-browser/node_modules/util": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
+ "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "2.0.3"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "1.1.60",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz",
+ "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==",
+ "dev": true
+ },
+ "node_modules/nodemon": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz",
+ "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.2.2",
+ "debug": "^3.2.6",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.0.4",
+ "pstree.remy": "^1.1.7",
+ "semver": "^5.7.1",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.2",
+ "update-notifier": "^4.0.0"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/nodemon/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/normalize-package-data/node_modules/hosted-git-info": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+ "dev": true
+ },
+ "node_modules/normalize-package-data/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
+ "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/npm-bundled": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
+ "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
+ "dev": true,
+ "dependencies": {
+ "npm-normalize-package-bin": "^1.0.1"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz",
+ "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
+ "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
+ "dev": true
+ },
+ "node_modules/npm-package-arg": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz",
+ "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^3.0.2",
+ "semver": "^7.0.0",
+ "validate-npm-package-name": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/npm-packlist": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
+ "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
+ "dev": true,
+ "dependencies": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1",
+ "npm-normalize-package-bin": "^1.0.1"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz",
+ "integrity": "sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw==",
+ "dev": true,
+ "dependencies": {
+ "npm-install-checks": "^4.0.0",
+ "npm-package-arg": "^8.0.0",
+ "semver": "^7.0.0"
+ }
+ },
+ "node_modules/npm-registry-fetch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz",
+ "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==",
+ "dev": true,
+ "dependencies": {
+ "bluebird": "^3.5.1",
+ "figgy-pudding": "^3.4.1",
+ "JSONStream": "^1.3.4",
+ "lru-cache": "^5.1.1",
+ "make-fetch-happen": "^5.0.0",
+ "npm-package-arg": "^6.1.0",
+ "safe-buffer": "^5.2.0"
+ }
+ },
+ "node_modules/npm-registry-fetch/node_modules/hosted-git-info": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+ "dev": true
+ },
+ "node_modules/npm-registry-fetch/node_modules/npm-package-arg": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz",
+ "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.7.1",
+ "osenv": "^0.1.5",
+ "semver": "^5.6.0",
+ "validate-npm-package-name": "^3.0.0"
+ }
+ },
+ "node_modules/npm-registry-fetch/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "node_modules/npm-registry-fetch/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "dev": true,
+ "dependencies": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "node_modules/nprogress": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
+ "integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
+ },
+ "node_modules/nth-check": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
+ "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
+ "dev": true,
+ "dependencies": {
+ "boolbase": "~1.0.0"
+ }
+ },
+ "node_modules/num2fraction": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+ "dev": true
+ },
+ "node_modules/number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
+ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
+ "dev": true
+ },
+ "node_modules/oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-component": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+ "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=",
+ "dev": true
+ },
+ "node_modules/object-copy": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+ "dev": true,
+ "dependencies": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
+ "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
+ "dev": true
+ },
+ "node_modules/object-is": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz",
+ "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-path": {
+ "version": "0.11.5",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.5.tgz",
+ "integrity": "sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.12.0"
+ }
+ },
+ "node_modules/object-visit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.getownpropertydescriptors": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
+ "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
+ "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+ "dev": true
+ },
+ "node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/open": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz",
+ "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/openurl": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz",
+ "integrity": "sha1-OHW0sO96UsFW8NtB1GCduw+Us4c=",
+ "dev": true
+ },
+ "node_modules/opn": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
+ "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==",
+ "dev": true,
+ "dependencies": {
+ "is-wsl": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/opn/node_modules/is-wsl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ora": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.4.tgz",
+ "integrity": "sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^3.0.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.2.0",
+ "is-interactive": "^1.0.0",
+ "log-symbols": "^3.0.0",
+ "mute-stream": "0.0.8",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/ora/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/ora/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/node_modules/supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/original": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
+ "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
+ "dev": true,
+ "dependencies": {
+ "url-parse": "^1.4.3"
+ }
+ },
+ "node_modules/os-browserify": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+ "dev": true
+ },
+ "node_modules/os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-locale": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
+ "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
+ "dev": true,
+ "dependencies": {
+ "execa": "^0.7.0",
+ "lcid": "^1.0.0",
+ "mem": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/os-locale/node_modules/cross-spawn": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^4.0.1",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "node_modules/os-locale/node_modules/execa": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+ "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^5.0.1",
+ "get-stream": "^3.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/os-locale/node_modules/get-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/os-locale/node_modules/lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "dev": true,
+ "dependencies": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "node_modules/os-locale/node_modules/yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+ "dev": true
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "dependencies": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "node_modules/outdent": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.7.1.tgz",
+ "integrity": "sha512-VjIzdUHunL74DdhcwMDt5FhNDQ8NYmTkuW0B+usIV2afS9aWT/1c9z1TsnFW349TP3nxmYeUl7Z++XpJRByvgg=="
+ },
+ "node_modules/p-cancelable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
+ "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
+ "dev": true,
+ "dependencies": {
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
+ "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
+ "dev": true,
+ "dependencies": {
+ "got": "^9.6.0",
+ "registry-auth-token": "^4.0.0",
+ "registry-url": "^5.0.0",
+ "semver": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/package-json/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/pacote": {
+ "version": "9.5.12",
+ "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.5.12.tgz",
+ "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==",
+ "dev": true,
+ "dependencies": {
+ "bluebird": "^3.5.3",
+ "cacache": "^12.0.2",
+ "chownr": "^1.1.2",
+ "figgy-pudding": "^3.5.1",
+ "get-stream": "^4.1.0",
+ "glob": "^7.1.3",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^5.1.1",
+ "make-fetch-happen": "^5.0.0",
+ "minimatch": "^3.0.4",
+ "minipass": "^2.3.5",
+ "mississippi": "^3.0.0",
+ "mkdirp": "^0.5.1",
+ "normalize-package-data": "^2.4.0",
+ "npm-normalize-package-bin": "^1.0.0",
+ "npm-package-arg": "^6.1.0",
+ "npm-packlist": "^1.1.12",
+ "npm-pick-manifest": "^3.0.0",
+ "npm-registry-fetch": "^4.0.0",
+ "osenv": "^0.1.5",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^1.1.1",
+ "protoduck": "^5.0.1",
+ "rimraf": "^2.6.2",
+ "safe-buffer": "^5.1.2",
+ "semver": "^5.6.0",
+ "ssri": "^6.0.1",
+ "tar": "^4.4.10",
+ "unique-filename": "^1.1.1",
+ "which": "^1.3.1"
+ }
+ },
+ "node_modules/pacote/node_modules/cacache": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz",
+ "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==",
+ "dev": true,
+ "dependencies": {
+ "bluebird": "^3.5.5",
+ "chownr": "^1.1.1",
+ "figgy-pudding": "^3.5.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.1.15",
+ "infer-owner": "^1.0.3",
+ "lru-cache": "^5.1.1",
+ "mississippi": "^3.0.0",
+ "mkdirp": "^0.5.1",
+ "move-concurrently": "^1.0.1",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^2.6.3",
+ "ssri": "^6.0.1",
+ "unique-filename": "^1.1.1",
+ "y18n": "^4.0.0"
+ }
+ },
+ "node_modules/pacote/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true
+ },
+ "node_modules/pacote/node_modules/hosted-git-info": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+ "dev": true
+ },
+ "node_modules/pacote/node_modules/minipass": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
+ "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "node_modules/pacote/node_modules/npm-package-arg": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz",
+ "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.7.1",
+ "osenv": "^0.1.5",
+ "semver": "^5.6.0",
+ "validate-npm-package-name": "^3.0.0"
+ }
+ },
+ "node_modules/pacote/node_modules/npm-pick-manifest": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz",
+ "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==",
+ "dev": true,
+ "dependencies": {
+ "figgy-pudding": "^3.5.1",
+ "npm-package-arg": "^6.0.0",
+ "semver": "^5.4.1"
+ }
+ },
+ "node_modules/pacote/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/pacote/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/pacote/node_modules/ssri": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
+ "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
+ "dev": true,
+ "dependencies": {
+ "figgy-pudding": "^3.5.1"
+ }
+ },
+ "node_modules/pacote/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "dev": true
+ },
+ "node_modules/parallel-transform": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
+ "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==",
+ "dev": true,
+ "dependencies": {
+ "cyclist": "^1.0.1",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.1.5"
+ }
+ },
+ "node_modules/parse-asn1": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
+ "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==",
+ "dev": true,
+ "dependencies": {
+ "asn1.js": "^5.2.0",
+ "browserify-aes": "^1.0.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+ "dev": true,
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
+ },
+ "node_modules/parseqs": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+ "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+ "dev": true,
+ "dependencies": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "node_modules/parseuri": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+ "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+ "dev": true,
+ "dependencies": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
+ "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==",
+ "dev": true
+ },
+ "node_modules/path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+ "dev": true
+ },
+ "node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+ "dev": true
+ },
+ "node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pbkdf2": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
+ "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
+ "dev": true,
+ "dependencies": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "dev": true,
+ "dependencies": {
+ "pinkie": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
+ "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pnp-webpack-plugin": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
+ "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==",
+ "dev": true,
+ "dependencies": {
+ "ts-pnp": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/portfinder": {
+ "version": "1.0.28",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
+ "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
+ "dev": true,
+ "dependencies": {
+ "async": "^2.6.2",
+ "debug": "^3.1.1",
+ "mkdirp": "^0.5.5"
+ },
+ "engines": {
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/portfinder/node_modules/debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/portscanner": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.1.1.tgz",
+ "integrity": "sha1-6rtAnk3iSVD1oqUW01rnaTQ/u5Y=",
+ "dev": true,
+ "dependencies": {
+ "async": "1.5.2",
+ "is-number-like": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=0.4",
+ "npm": ">=1.0.0"
+ }
+ },
+ "node_modules/portscanner/node_modules/async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+ "dev": true
+ },
+ "node_modules/posix-character-classes": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "7.0.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.31.tgz",
+ "integrity": "sha512-a937VDHE1ftkjk+8/7nj/mrjtmkn69xxzJgRETXdAUU+IgOYPQNJF17haGWbeDxSyk++HA14UA98FurvPyBJOA==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.2",
+ "source-map": "^0.6.1",
+ "supports-color": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/postcss-calc": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.3.tgz",
+ "integrity": "sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.27",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
+ "node_modules/postcss-colormin": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz",
+ "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "color": "^3.0.0",
+ "has": "^1.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-colormin/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-convert-values": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz",
+ "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-convert-values/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-discard-comments": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz",
+ "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-discard-duplicates": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz",
+ "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-discard-empty": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz",
+ "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-discard-overridden": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz",
+ "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz",
+ "integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.1",
+ "postcss-value-parser": "^3.2.3",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/postcss-import/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-load-config": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz",
+ "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==",
+ "dev": true,
+ "dependencies": {
+ "cosmiconfig": "^5.0.0",
+ "import-cwd": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/postcss-loader": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz",
+ "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^1.1.0",
+ "postcss": "^7.0.0",
+ "postcss-load-config": "^2.0.0",
+ "schema-utils": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/postcss-loader/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/postcss-loader/node_modules/schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/postcss-merge-longhand": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz",
+ "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==",
+ "dev": true,
+ "dependencies": {
+ "css-color-names": "0.0.4",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0",
+ "stylehacks": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-merge-rules": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz",
+ "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "caniuse-api": "^3.0.0",
+ "cssnano-util-same-parent": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-selector-parser": "^3.0.0",
+ "vendors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
+ "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
+ "dev": true,
+ "dependencies": {
+ "dot-prop": "^5.2.0",
+ "indexes-of": "^1.0.1",
+ "uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/postcss-minify-font-values": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz",
+ "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-minify-gradients": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz",
+ "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==",
+ "dev": true,
+ "dependencies": {
+ "cssnano-util-get-arguments": "^4.0.0",
+ "is-color-stop": "^1.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-minify-params": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz",
+ "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==",
+ "dev": true,
+ "dependencies": {
+ "alphanum-sort": "^1.0.0",
+ "browserslist": "^4.0.0",
+ "cssnano-util-get-arguments": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0",
+ "uniqs": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-minify-params/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-minify-selectors": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz",
+ "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==",
+ "dev": true,
+ "dependencies": {
+ "alphanum-sort": "^1.0.0",
+ "has": "^1.0.0",
+ "postcss": "^7.0.0",
+ "postcss-selector-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
+ "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
+ "dev": true,
+ "dependencies": {
+ "dot-prop": "^5.2.0",
+ "indexes-of": "^1.0.1",
+ "uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz",
+ "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz",
+ "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==",
+ "dev": true,
+ "dependencies": {
+ "icss-utils": "^4.1.1",
+ "postcss": "^7.0.32",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default/node_modules/postcss": {
+ "version": "7.0.32",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
+ "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.2",
+ "source-map": "^0.6.1",
+ "supports-color": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default/node_modules/supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz",
+ "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.6",
+ "postcss-selector-parser": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz",
+ "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==",
+ "dev": true,
+ "dependencies": {
+ "icss-utils": "^4.0.0",
+ "postcss": "^7.0.6"
+ }
+ },
+ "node_modules/postcss-normalize-charset": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz",
+ "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-display-values": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz",
+ "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==",
+ "dev": true,
+ "dependencies": {
+ "cssnano-util-get-match": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-normalize-positions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz",
+ "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==",
+ "dev": true,
+ "dependencies": {
+ "cssnano-util-get-arguments": "^4.0.0",
+ "has": "^1.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-normalize-repeat-style": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz",
+ "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==",
+ "dev": true,
+ "dependencies": {
+ "cssnano-util-get-arguments": "^4.0.0",
+ "cssnano-util-get-match": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-normalize-string": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz",
+ "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-normalize-timing-functions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz",
+ "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==",
+ "dev": true,
+ "dependencies": {
+ "cssnano-util-get-match": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-normalize-unicode": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz",
+ "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-normalize-url": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz",
+ "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==",
+ "dev": true,
+ "dependencies": {
+ "is-absolute-url": "^2.0.0",
+ "normalize-url": "^3.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-normalize-whitespace": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz",
+ "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-ordered-values": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz",
+ "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==",
+ "dev": true,
+ "dependencies": {
+ "cssnano-util-get-arguments": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-reduce-initial": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz",
+ "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "caniuse-api": "^3.0.0",
+ "has": "^1.0.0",
+ "postcss": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-reduce-transforms": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz",
+ "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==",
+ "dev": true,
+ "dependencies": {
+ "cssnano-util-get-match": "^4.0.0",
+ "has": "^1.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
+ "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "indexes-of": "^1.0.1",
+ "uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-svgo": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz",
+ "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==",
+ "dev": true,
+ "dependencies": {
+ "is-svg": "^3.0.0",
+ "postcss": "^7.0.0",
+ "postcss-value-parser": "^3.0.0",
+ "svgo": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-svgo/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-unique-selectors": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz",
+ "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==",
+ "dev": true,
+ "dependencies": {
+ "alphanum-sort": "^1.0.0",
+ "postcss": "^7.0.0",
+ "uniqs": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
+ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
+ "dev": true
+ },
+ "node_modules/postcss/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss/node_modules/supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prepend-http": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
+ "dev": true
+ },
+ "node_modules/promise-retry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz",
+ "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=",
+ "dev": true,
+ "dependencies": {
+ "err-code": "^1.0.0",
+ "retry": "^0.10.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/promise-retry/node_modules/retry": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
+ "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/protoduck": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz",
+ "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==",
+ "dev": true,
+ "dependencies": {
+ "genfun": "^5.0.0"
+ }
+ },
+ "node_modules/protractor": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz",
+ "integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==",
+ "dev": true,
+ "dependencies": {
+ "@types/q": "^0.0.32",
+ "@types/selenium-webdriver": "^3.0.0",
+ "blocking-proxy": "^1.0.0",
+ "browserstack": "^1.5.1",
+ "chalk": "^1.1.3",
+ "glob": "^7.0.3",
+ "jasmine": "2.8.0",
+ "jasminewd2": "^2.1.0",
+ "q": "1.4.1",
+ "saucelabs": "^1.5.0",
+ "selenium-webdriver": "3.6.0",
+ "source-map-support": "~0.4.0",
+ "webdriver-js-extender": "2.1.0",
+ "webdriver-manager": "^12.1.7",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "protractor": "bin/protractor",
+ "webdriver-manager": "bin/webdriver-manager"
+ },
+ "engines": {
+ "node": ">=10.13.x"
+ }
+ },
+ "node_modules/protractor/node_modules/@types/q": {
+ "version": "0.0.32",
+ "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
+ "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
+ "dev": true
+ },
+ "node_modules/protractor/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+ "dev": true,
+ "dependencies": {
+ "array-uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/protractor/node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/protractor/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/protractor/node_modules/del": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+ "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+ "dev": true,
+ "dependencies": {
+ "globby": "^5.0.0",
+ "is-path-cwd": "^1.0.0",
+ "is-path-in-cwd": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "rimraf": "^2.2.8"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/protractor/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/globby": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+ "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^1.0.1",
+ "arrify": "^1.0.0",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/is-path-cwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+ "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/is-path-in-cwd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+ "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+ "dev": true,
+ "dependencies": {
+ "is-path-inside": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/is-path-inside": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+ "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+ "dev": true,
+ "dependencies": {
+ "path-is-inside": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/jasmine": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz",
+ "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=",
+ "dev": true,
+ "dependencies": {
+ "exit": "^0.1.2",
+ "glob": "^7.0.6",
+ "jasmine-core": "~2.8.0"
+ },
+ "bin": {
+ "jasmine": "bin/jasmine.js"
+ }
+ },
+ "node_modules/protractor/node_modules/jasmine-core": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz",
+ "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
+ "dev": true
+ },
+ "node_modules/protractor/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/q": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
+ "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.0",
+ "teleport": ">=0.2.0"
+ }
+ },
+ "node_modules/protractor/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/protractor/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/protractor/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/protractor/node_modules/source-map-support": {
+ "version": "0.4.18",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+ "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+ "dev": true,
+ "dependencies": {
+ "source-map": "^0.5.6"
+ }
+ },
+ "node_modules/protractor/node_modules/string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/string-width/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/protractor/node_modules/webdriver-manager": {
+ "version": "12.1.7",
+ "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.7.tgz",
+ "integrity": "sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA==",
+ "dev": true,
+ "dependencies": {
+ "adm-zip": "^0.4.9",
+ "chalk": "^1.1.1",
+ "del": "^2.2.0",
+ "glob": "^7.0.3",
+ "ini": "^1.3.4",
+ "minimist": "^1.2.0",
+ "q": "^1.4.1",
+ "request": "^2.87.0",
+ "rimraf": "^2.5.2",
+ "semver": "^5.3.0",
+ "xml2js": "^0.4.17"
+ },
+ "bin": {
+ "webdriver-manager": "bin/webdriver-manager"
+ },
+ "engines": {
+ "node": ">=6.9.x"
+ }
+ },
+ "node_modules/protractor/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/protractor/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
+ "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
+ "dependencies": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+ "devOptional": true
+ },
+ "node_modules/pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+ "dev": true
+ },
+ "node_modules/psl": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
+ "dev": true
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
+ },
+ "node_modules/public-encrypt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/public-encrypt/node_modules/bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/pumpify": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+ "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+ "dev": true,
+ "dependencies": {
+ "duplexify": "^3.6.0",
+ "inherits": "^2.0.3",
+ "pump": "^2.0.0"
+ }
+ },
+ "node_modules/pumpify/node_modules/pump": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+ "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pupa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz",
+ "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==",
+ "dev": true,
+ "dependencies": {
+ "escape-goat": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/q": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.0",
+ "teleport": ">=0.2.0"
+ }
+ },
+ "node_modules/qjobs": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
+ "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.9"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/query-string": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
+ "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
+ "dev": true,
+ "dependencies": {
+ "object-assign": "^4.1.0",
+ "strict-uri-encode": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/querystring-es3": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+ "dependencies": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/raw-loader": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.1.tgz",
+ "integrity": "sha512-baolhQBSi3iNh1cglJjA0mYzga+wePk7vdEX//1dTFd+v4TsQlQE0jitJSNF1OIP82rdYulH7otaVmdlDaJ64A==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.5"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dev": true,
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
+ "dev": true,
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/read-cache/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-package-json": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.1.tgz",
+ "integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.1",
+ "graceful-fs": "^4.1.2",
+ "json-parse-better-errors": "^1.0.1",
+ "normalize-package-data": "^2.0.0",
+ "npm-normalize-package-bin": "^1.0.0"
+ }
+ },
+ "node_modules/read-package-tree": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz",
+ "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==",
+ "dev": true,
+ "dependencies": {
+ "read-package-json": "^2.0.0",
+ "readdir-scoped-modules": "^1.0.0",
+ "util-promisify": "^2.1.0"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
+ "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
+ "dev": true,
+ "dependencies": {
+ "load-json-file": "^2.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
+ "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^2.0.0",
+ "read-pkg": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg/node_modules/path-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
+ "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
+ "dev": true,
+ "dependencies": {
+ "pify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/read-pkg/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readdir-scoped-modules": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz",
+ "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==",
+ "dev": true,
+ "dependencies": {
+ "debuglog": "^1.0.1",
+ "dezalgo": "^1.0.0",
+ "graceful-fs": "^4.1.2",
+ "once": "^1.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+ "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reflect-metadata": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
+ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
+ "dev": true
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz",
+ "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==",
+ "dev": true
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz",
+ "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==",
+ "dev": true,
+ "dependencies": {
+ "regenerate": "^1.4.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.5",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
+ "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
+ "dev": true
+ },
+ "node_modules/regenerator-transform": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz",
+ "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.8.4"
+ }
+ },
+ "node_modules/regex-not": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regex-parser": {
+ "version": "2.2.10",
+ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.10.tgz",
+ "integrity": "sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA==",
+ "dev": true
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
+ "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz",
+ "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==",
+ "dev": true,
+ "dependencies": {
+ "regenerate": "^1.4.0",
+ "regenerate-unicode-properties": "^8.2.0",
+ "regjsgen": "^0.5.1",
+ "regjsparser": "^0.6.4",
+ "unicode-match-property-ecmascript": "^1.0.4",
+ "unicode-match-property-value-ecmascript": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/registry-auth-token": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz",
+ "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==",
+ "dev": true,
+ "dependencies": {
+ "rc": "^1.2.8"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/registry-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
+ "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
+ "dev": true,
+ "dependencies": {
+ "rc": "^1.2.8"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/regjsgen": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz",
+ "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==",
+ "dev": true
+ },
+ "node_modules/regjsparser": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz",
+ "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==",
+ "dev": true,
+ "dependencies": {
+ "jsesc": "~0.5.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/regjsparser/node_modules/jsesc": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ }
+ },
+ "node_modules/remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+ "dev": true
+ },
+ "node_modules/repeat-element": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+ "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/request": {
+ "version": "2.88.2",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
+ "dev": true,
+ "dependencies": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/request-promise-core": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
+ "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.19"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/request-promise-native": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz",
+ "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==",
+ "dev": true,
+ "dependencies": {
+ "request-promise-core": "1.1.4",
+ "stealthy-require": "^1.1.1",
+ "tough-cookie": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/request/node_modules/qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+ "dev": true
+ },
+ "node_modules/resolve": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+ "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
+ "dev": true,
+ "dependencies": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
+ "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
+ "dev": true,
+ "dependencies": {
+ "resolve-from": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+ "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+ "dev": true
+ },
+ "node_modules/resolve-url-loader": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz",
+ "integrity": "sha512-K1N5xUjj7v0l2j/3Sgs5b8CjrrgtC70SmdCuZiJ8tSyb5J+uk3FoeZ4b7yTnH6j7ngI+Bc5bldHJIa8hYdu2gQ==",
+ "dev": true,
+ "dependencies": {
+ "adjust-sourcemap-loader": "2.0.0",
+ "camelcase": "5.3.1",
+ "compose-function": "3.0.3",
+ "convert-source-map": "1.7.0",
+ "es6-iterator": "2.0.3",
+ "loader-utils": "1.2.3",
+ "postcss": "7.0.21",
+ "rework": "1.0.1",
+ "rework-visit": "1.0.0",
+ "source-map": "0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/loader-utils": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^2.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/postcss": {
+ "version": "7.0.21",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz",
+ "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.2",
+ "source-map": "^0.6.1",
+ "supports-color": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/resp-modifier": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz",
+ "integrity": "sha1-sSTeXE+6/LpUH0j/pzlw9KpFa08=",
+ "dev": true,
+ "dependencies": {
+ "debug": "^2.2.0",
+ "minimatch": "^3.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/resp-modifier/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/resp-modifier/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/responselike": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+ "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
+ "dev": true,
+ "dependencies": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rework": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz",
+ "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=",
+ "dev": true,
+ "dependencies": {
+ "convert-source-map": "^0.3.3",
+ "css": "^2.0.0"
+ }
+ },
+ "node_modules/rework-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz",
+ "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=",
+ "dev": true
+ },
+ "node_modules/rework/node_modules/convert-source-map": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
+ "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
+ "dev": true
+ },
+ "node_modules/rfdc": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz",
+ "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==",
+ "dev": true
+ },
+ "node_modules/rgb-regex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
+ "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=",
+ "dev": true
+ },
+ "node_modules/rgba-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
+ "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=",
+ "dev": true
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/ripemd160": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "dev": true,
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.10.9",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.10.9.tgz",
+ "integrity": "sha512-dY/EbjiWC17ZCUSyk14hkxATAMAShkMsD43XmZGWjLrgFj15M3Dw2kEkA9ns64BiLFm9PKN6vTQw8neHwK74eg==",
+ "dev": true,
+ "dependencies": {
+ "fsevents": "~2.1.2"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.1.2"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
+ "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
+ "dev": true
+ },
+ "node_modules/run-queue": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
+ "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
+ "dev": true,
+ "dependencies": {
+ "aproba": "^1.1.1"
+ }
+ },
+ "node_modules/rx": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
+ "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=",
+ "dev": true
+ },
+ "node_modules/rxjs": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
+ "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/rxjs/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/safe-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+ "dev": true,
+ "dependencies": {
+ "ret": "~0.1.10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/sass": {
+ "version": "1.26.5",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.5.tgz",
+ "integrity": "sha512-FG2swzaZUiX53YzZSjSakzvGtlds0lcbF+URuU9mxOv7WBh7NhXEVDa4kPKN4hN6fC2TkOTOKqiqp6d53N9X5Q==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": ">=2.0.0 <4.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/sass-loader": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz",
+ "integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==",
+ "dev": true,
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "loader-utils": "^1.2.3",
+ "neo-async": "^2.6.1",
+ "schema-utils": "^2.6.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/sass-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/sass-loader/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/sass-loader/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/saucelabs": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz",
+ "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==",
+ "dev": true,
+ "dependencies": {
+ "https-proxy-agent": "^2.2.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+ "dev": true
+ },
+ "node_modules/saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dev": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/schema-utils": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
+ "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.4",
+ "ajv": "^6.12.2",
+ "ajv-keywords": "^3.4.1"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/scratch-blocks": {
+ "version": "0.1.0-prerelease.20200512201140",
+ "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20200512201140.tgz",
+ "integrity": "sha512-lNTp5bxl/aHiN1I4bosEHj2MuiiKI8K714jgU8bUppWpNvQwp1PFRdoz/wjC8cjjReVBR37ZlkFQ5QzzJSULfA==",
+ "dev": true,
+ "dependencies": {
+ "exports-loader": "0.6.3",
+ "imports-loader": "0.6.5"
+ }
+ },
+ "node_modules/select-hose": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+ "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
+ "dev": true
+ },
+ "node_modules/selenium-webdriver": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz",
+ "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==",
+ "dev": true,
+ "dependencies": {
+ "jszip": "^3.1.3",
+ "rimraf": "^2.5.4",
+ "tmp": "0.0.30",
+ "xml2js": "^0.4.17"
+ },
+ "engines": {
+ "node": ">= 6.9.0"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/tmp": {
+ "version": "0.0.30",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
+ "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=",
+ "dev": true,
+ "dependencies": {
+ "os-tmpdir": "~1.0.1"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/selfsigned": {
+ "version": "1.10.8",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
+ "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
+ "dev": true,
+ "dependencies": {
+ "node-forge": "^0.10.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
+ "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver-diff": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
+ "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/semver-diff/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/semver-dsl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz",
+ "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=",
+ "dev": true,
+ "dependencies": {
+ "semver": "^5.3.0"
+ }
+ },
+ "node_modules/semver-dsl/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/semver-intersect": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.4.0.tgz",
+ "integrity": "sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^5.0.0"
+ }
+ },
+ "node_modules/semver-intersect/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "node_modules/serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.6.2",
+ "mime-types": "~2.1.17",
+ "parseurl": "~1.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "dev": true,
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "node_modules/serve-index/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/serve-index/node_modules/setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+ "dev": true
+ },
+ "node_modules/serve-static": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/server-destroy": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
+ "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=",
+ "dev": true
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "node_modules/set-immediate-shim": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+ "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/set-value": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/set-value/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+ "dev": true
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "node_modules/sha.js": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ }
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
+ "dev": true
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz",
+ "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/snapdragon": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+ "dev": true,
+ "dependencies": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+ "dev": true,
+ "dependencies": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/snapdragon/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
+ "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "~4.1.0",
+ "engine.io": "~3.4.0",
+ "has-binary2": "~1.0.2",
+ "socket.io-adapter": "~1.1.0",
+ "socket.io-client": "2.3.0",
+ "socket.io-parser": "~3.4.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz",
+ "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==",
+ "dev": true
+ },
+ "node_modules/socket.io-client": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz",
+ "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==",
+ "dev": true,
+ "dependencies": {
+ "backo2": "1.0.2",
+ "base64-arraybuffer": "0.1.5",
+ "component-bind": "1.0.0",
+ "component-emitter": "1.2.1",
+ "debug": "~4.1.0",
+ "engine.io-client": "~3.4.0",
+ "has-binary2": "~1.0.2",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "object-component": "0.0.3",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "socket.io-parser": "~3.3.0",
+ "to-array": "0.1.4"
+ }
+ },
+ "node_modules/socket.io-client/node_modules/component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+ "dev": true
+ },
+ "node_modules/socket.io-client/node_modules/isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+ "dev": true
+ },
+ "node_modules/socket.io-client/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/socket.io-client/node_modules/socket.io-parser": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
+ "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
+ "dev": true,
+ "dependencies": {
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "isarray": "2.0.1"
+ }
+ },
+ "node_modules/socket.io-client/node_modules/socket.io-parser/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
+ "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
+ "dev": true,
+ "dependencies": {
+ "component-emitter": "1.2.1",
+ "debug": "~4.1.0",
+ "isarray": "2.0.1"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+ "dev": true
+ },
+ "node_modules/socket.io-parser/node_modules/isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+ "dev": true
+ },
+ "node_modules/sockjs": {
+ "version": "0.3.20",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
+ "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==",
+ "dev": true,
+ "dependencies": {
+ "faye-websocket": "^0.10.0",
+ "uuid": "^3.4.0",
+ "websocket-driver": "0.6.5"
+ }
+ },
+ "node_modules/sockjs-client": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz",
+ "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.5",
+ "eventsource": "^1.0.7",
+ "faye-websocket": "~0.11.1",
+ "inherits": "^2.0.3",
+ "json3": "^3.3.2",
+ "url-parse": "^1.4.3"
+ }
+ },
+ "node_modules/sockjs-client/node_modules/debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/sockjs-client/node_modules/faye-websocket": {
+ "version": "0.11.3",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz",
+ "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==",
+ "dev": true,
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz",
+ "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==",
+ "dev": true,
+ "dependencies": {
+ "ip": "1.1.5",
+ "smart-buffer": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz",
+ "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "~4.2.1",
+ "socks": "~2.3.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/agent-base": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
+ "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
+ "dev": true,
+ "dependencies": {
+ "es6-promisify": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/sort-keys": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
+ "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
+ "dev": true,
+ "dependencies": {
+ "is-plain-obj": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-list-map": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+ "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+ "dev": true
+ },
+ "node_modules/source-map": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/source-map-loader": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.0.0.tgz",
+ "integrity": "sha512-ZayyQCSCrQazN50aCvuS84lJT4xc1ZAcykH5blHaBdVveSwjiFK8UGMPvao0ho54DTb0Jf7m57uRRG/YYUZ2Fg==",
+ "dev": true,
+ "dependencies": {
+ "data-urls": "^2.0.0",
+ "iconv-lite": "^0.5.1",
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.6",
+ "source-map": "^0.6.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/source-map-loader/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-resolve": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
+ "dev": true,
+ "dependencies": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-url": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+ "dev": true
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
+ "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+ "dev": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+ "dev": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+ "dev": true
+ },
+ "node_modules/spdy": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.0",
+ "handle-thing": "^2.0.0",
+ "http-deceiver": "^1.2.7",
+ "select-hose": "^2.0.0",
+ "spdy-transport": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/spdy-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+ "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.0",
+ "detect-node": "^2.0.4",
+ "hpack.js": "^2.1.6",
+ "obuf": "^1.1.2",
+ "readable-stream": "^3.0.6",
+ "wbuf": "^1.7.3"
+ }
+ },
+ "node_modules/spdy-transport/node_modules/readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/speed-measure-webpack-plugin": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.3.tgz",
+ "integrity": "sha512-2ljD4Ch/rz2zG3HsLsnPfp23osuPBS0qPuz9sGpkNXTN1Ic4M+W9xB8l8rS8ob2cO4b1L+WTJw/0AJwWYVgcxQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/split-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ },
+ "node_modules/sshpk": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+ "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+ "dev": true,
+ "dependencies": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ },
+ "bin": {
+ "sshpk-conv": "bin/sshpk-conv",
+ "sshpk-sign": "bin/sshpk-sign",
+ "sshpk-verify": "bin/sshpk-verify"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ssri": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz",
+ "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/stable": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
+ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
+ "dev": true
+ },
+ "node_modules/stack-generator": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz",
+ "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==",
+ "dependencies": {
+ "stackframe": "^1.1.1"
+ }
+ },
+ "node_modules/stackframe": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
+ "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
+ },
+ "node_modules/static-extend": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+ "dev": true,
+ "dependencies": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/stealthy-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
+ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stream-browserify": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
+ "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "~2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/stream-each": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
+ "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "node_modules/stream-http": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+ "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+ "dev": true,
+ "dependencies": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.3.6",
+ "to-arraybuffer": "^1.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
+ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
+ "dev": true
+ },
+ "node_modules/stream-throttle": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz",
+ "integrity": "sha1-rdV8jXzHOoFjDTHNVdOWHPr7qcM=",
+ "dev": true,
+ "dependencies": {
+ "commander": "^2.2.0",
+ "limiter": "^1.0.5"
+ },
+ "bin": {
+ "throttleproxy": "bin/throttleproxy.js"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/streamroller": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz",
+ "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==",
+ "dev": true,
+ "dependencies": {
+ "date-format": "^2.1.0",
+ "debug": "^4.1.1",
+ "fs-extra": "^8.1.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/streamroller/node_modules/date-format": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
+ "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/streamroller/node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/strict-uri-encode": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
+ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "devOptional": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+ "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+ "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/style-loader": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz",
+ "integrity": "sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.6"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ }
+ },
+ "node_modules/stylehacks": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
+ "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "postcss": "^7.0.0",
+ "postcss-selector-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/stylehacks/node_modules/postcss-selector-parser": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
+ "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
+ "dev": true,
+ "dependencies": {
+ "dot-prop": "^5.2.0",
+ "indexes-of": "^1.0.1",
+ "uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stylus": {
+ "version": "0.54.7",
+ "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.7.tgz",
+ "integrity": "sha512-Yw3WMTzVwevT6ZTrLCYNHAFmanMxdylelL3hkWNgPMeTCpMwpV3nXjpOHuBXtFv7aiO2xRuQS6OoAdgkNcSNug==",
+ "dev": true,
+ "dependencies": {
+ "css-parse": "~2.0.0",
+ "debug": "~3.1.0",
+ "glob": "^7.1.3",
+ "mkdirp": "~0.5.x",
+ "safer-buffer": "^2.1.2",
+ "sax": "~1.2.4",
+ "semver": "^6.0.0",
+ "source-map": "^0.7.3"
+ },
+ "bin": {
+ "stylus": "bin/stylus"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/stylus-loader": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz",
+ "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^1.0.2",
+ "lodash.clonedeep": "^4.5.0",
+ "when": "~3.6.x"
+ }
+ },
+ "node_modules/stylus-loader/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/stylus-loader/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/stylus/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/stylus/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/stylus/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/svgo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
+ "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.1",
+ "coa": "^2.0.2",
+ "css-select": "^2.0.0",
+ "css-select-base-adapter": "^0.1.1",
+ "css-tree": "1.0.0-alpha.37",
+ "csso": "^4.0.2",
+ "js-yaml": "^3.13.1",
+ "mkdirp": "~0.5.1",
+ "object.values": "^1.1.0",
+ "sax": "~1.2.4",
+ "stable": "^0.1.8",
+ "unquote": "~1.1.1",
+ "util.promisify": "~1.0.0"
+ },
+ "bin": {
+ "svgo": "bin/svgo"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
+ "node_modules/tapable": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
+ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar": {
+ "version": "4.4.13",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
+ "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
+ "dev": true,
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.8.6",
+ "minizlib": "^1.2.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=4.5"
+ }
+ },
+ "node_modules/tar/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true
+ },
+ "node_modules/tar/node_modules/fs-minipass": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
+ "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^2.6.0"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
+ "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "node_modules/tar/node_modules/minizlib": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
+ "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^2.9.0"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/term-size": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz",
+ "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/terser": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz",
+ "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==",
+ "dev": true,
+ "dependencies": {
+ "commander": "^2.20.0",
+ "source-map": "~0.6.1",
+ "source-map-support": "~0.5.12"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.0.1.tgz",
+ "integrity": "sha512-eFDtq8qPUEa9hXcUzTwKXTnugIVtlqc1Z/ZVhG8LmRT3lgRY13+pQTnFLY2N7ATB6TKCHuW/IGjoAnZz9wOIqw==",
+ "dev": true,
+ "dependencies": {
+ "cacache": "^15.0.3",
+ "find-cache-dir": "^3.3.1",
+ "jest-worker": "^26.0.0",
+ "p-limit": "^2.3.0",
+ "schema-utils": "^2.6.6",
+ "serialize-javascript": "^3.0.0",
+ "source-map": "^0.6.1",
+ "terser": "^4.6.13",
+ "webpack-sources": "^1.4.3"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
+ "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/terser/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tfunk": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tfunk/-/tfunk-4.0.0.tgz",
+ "integrity": "sha512-eJQ0dGfDIzWNiFNYFVjJ+Ezl/GmwHaFTBTjrtqNPW0S7cuVDBrZrmzUz6VkMeCR4DZFqhd4YtLwsw3i2wYHswQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^1.1.3",
+ "dlv": "^1.1.3"
+ }
+ },
+ "node_modules/tfunk/node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tfunk/node_modules/chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tfunk/node_modules/supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "dev": true,
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/thunky": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+ "dev": true
+ },
+ "node_modules/timers-browserify": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz",
+ "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==",
+ "dev": true,
+ "dependencies": {
+ "setimmediate": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/timsort": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
+ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
+ "dev": true
+ },
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/to-array": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+ "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=",
+ "dev": true
+ },
+ "node_modules/to-arraybuffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+ "dev": true
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-object-path/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-readable-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+ "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/to-regex": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+ "dev": true,
+ "dependencies": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+ "dev": true,
+ "dependencies": {
+ "nopt": "~1.0.10"
+ },
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/touch/node_modules/nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "dev": true,
+ "dependencies": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
+ "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "8.10.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
+ "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
+ "dev": true,
+ "dependencies": {
+ "arg": "^4.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/ts-pnp": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
+ "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ },
+ "node_modules/tslint": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
+ "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.3",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.13.0",
+ "tsutils": "^2.29.0"
+ },
+ "bin": {
+ "tslint": "bin/tslint"
+ },
+ "engines": {
+ "node": ">=4.8.0"
+ }
+ },
+ "node_modules/tslint/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/tslint/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ },
+ "node_modules/tty-browserify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+ "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+ "dev": true
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true
+ },
+ "node_modules/type": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+ "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
+ "dev": true
+ },
+ "node_modules/type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
+ "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+ "dev": true
+ },
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "3.9.7",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
+ "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/ua-parser-js": {
+ "version": "0.7.21",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
+ "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ultron": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+ "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
+ "dev": true
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz",
+ "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^2.2.0"
+ }
+ },
+ "node_modules/undefsafe/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/undefsafe/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
+ "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
+ "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
+ "dev": true,
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^1.0.4",
+ "unicode-property-aliases-ecmascript": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz",
+ "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz",
+ "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/union-value": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+ "dev": true,
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unipointer": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/unipointer/-/unipointer-2.3.0.tgz",
+ "integrity": "sha512-m85sAoELCZhogI1owtJV3Dva7GxkHk2lI7A0otw3o0OwCuC/Q9gi7ehddigEYIAYbhkqNdri+dU1QQkrcBvirQ==",
+ "dependencies": {
+ "ev-emitter": "^1.0.1"
+ }
+ },
+ "node_modules/uniq": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+ "dev": true
+ },
+ "node_modules/uniqs": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz",
+ "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=",
+ "dev": true
+ },
+ "node_modules/unique-filename": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
+ "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
+ "dev": true,
+ "dependencies": {
+ "unique-slug": "^2.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz",
+ "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ }
+ },
+ "node_modules/unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "dev": true,
+ "dependencies": {
+ "crypto-random-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/universal-analytics": {
+ "version": "0.4.20",
+ "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.20.tgz",
+ "integrity": "sha512-gE91dtMvNkjO+kWsPstHRtSwHXz0l2axqptGYp5ceg4MsuurloM0PU3pdOfpb5zBXUvyjT4PwhWK2m39uczZuw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.0.0",
+ "request": "^2.88.0",
+ "uuid": "^3.0.0"
+ }
+ },
+ "node_modules/universal-analytics/node_modules/debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unquote": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
+ "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
+ "dev": true
+ },
+ "node_modules/unset-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+ "dev": true,
+ "dependencies": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+ "dev": true,
+ "dependencies": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value/node_modules/isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+ "dev": true,
+ "dependencies": {
+ "isarray": "1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-values": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+ "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4",
+ "yarn": "*"
+ }
+ },
+ "node_modules/update-notifier": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.1.tgz",
+ "integrity": "sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg==",
+ "dev": true,
+ "dependencies": {
+ "boxen": "^4.2.0",
+ "chalk": "^3.0.0",
+ "configstore": "^5.0.1",
+ "has-yarn": "^2.1.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.3.1",
+ "is-npm": "^4.0.0",
+ "is-yarn-global": "^0.3.0",
+ "latest-version": "^5.0.0",
+ "pupa": "^2.0.1",
+ "semver-diff": "^3.1.1",
+ "xdg-basedir": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-notifier/node_modules/ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-notifier/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-notifier/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/update-notifier/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/update-notifier/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-notifier/node_modules/supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+ "dev": true
+ },
+ "node_modules/url": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+ "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+ "dev": true,
+ "dependencies": {
+ "punycode": "1.3.2",
+ "querystring": "0.2.0"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
+ "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
+ "dev": true,
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/url-parse-lax": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+ "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
+ "dev": true,
+ "dependencies": {
+ "prepend-http": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/url-parse-lax/node_modules/prepend-http": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/url/node_modules/punycode": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+ "dev": true
+ },
+ "node_modules/use": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+ "dev": true,
+ "dependencies": {
+ "inherits": "2.0.1"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "devOptional": true
+ },
+ "node_modules/util-promisify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz",
+ "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=",
+ "dev": true,
+ "dependencies": {
+ "object.getownpropertydescriptors": "^2.0.3"
+ }
+ },
+ "node_modules/util.promisify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz",
+ "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.2",
+ "has-symbols": "^1.0.1",
+ "object.getownpropertydescriptors": "^2.1.0"
+ }
+ },
+ "node_modules/util/node_modules/inherits": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+ "dev": true
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+ "dev": true,
+ "bin": {
+ "uuid": "bin/uuid"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
+ "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
+ "dev": true,
+ "dependencies": {
+ "builtins": "^1.0.3"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vendors": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz",
+ "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==",
+ "dev": true
+ },
+ "node_modules/verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "node_modules/vm-browserify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+ "dev": true
+ },
+ "node_modules/void-elements": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
+ "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/w3c-hr-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+ "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+ "dev": true,
+ "dependencies": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
+ "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
+ "dev": true,
+ "dependencies": {
+ "xml-name-validator": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz",
+ "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.4.1",
+ "graceful-fs": "^4.1.2",
+ "neo-async": "^2.5.0",
+ "watchpack-chokidar2": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "watchpack-chokidar2": "^2.0.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz",
+ "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "chokidar": "^2.1.8"
+ },
+ "engines": {
+ "node": "<8.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/anymatch/node_modules/normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "remove-trailing-separator": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/braces/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/chokidar": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
+ "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "fsevents": "^1.2.7",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/fill-range/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/glob-parent/node_modules/is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "binary-extensions": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/watchpack-chokidar2/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wbuf": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+ "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+ "dev": true,
+ "dependencies": {
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
+ "dev": true,
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/webdriver-js-extender": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz",
+ "integrity": "sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/selenium-webdriver": "^3.0.0",
+ "selenium-webdriver": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=6.9.x"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+ "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/webpack": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz",
+ "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==",
+ "dev": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-module-context": "1.9.0",
+ "@webassemblyjs/wasm-edit": "1.9.0",
+ "@webassemblyjs/wasm-parser": "1.9.0",
+ "acorn": "^6.4.1",
+ "ajv": "^6.10.2",
+ "ajv-keywords": "^3.4.1",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^4.1.0",
+ "eslint-scope": "^4.0.3",
+ "json-parse-better-errors": "^1.0.2",
+ "loader-runner": "^2.4.0",
+ "loader-utils": "^1.2.3",
+ "memory-fs": "^0.4.1",
+ "micromatch": "^3.1.10",
+ "mkdirp": "^0.5.3",
+ "neo-async": "^2.6.1",
+ "node-libs-browser": "^2.2.1",
+ "schema-utils": "^1.0.0",
+ "tapable": "^1.1.3",
+ "terser-webpack-plugin": "^1.4.3",
+ "watchpack": "^1.6.1",
+ "webpack-sources": "^1.4.1"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
+ "node_modules/webpack-dev-middleware": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz",
+ "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==",
+ "dev": true,
+ "dependencies": {
+ "memory-fs": "^0.4.1",
+ "mime": "^2.4.4",
+ "mkdirp": "^0.5.1",
+ "range-parser": "^1.2.1",
+ "webpack-log": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/webpack-dev-middleware/node_modules/memory-fs": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+ "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+ "dev": true,
+ "dependencies": {
+ "errno": "^0.1.3",
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "node_modules/webpack-dev-middleware/node_modules/mime": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
+ "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/webpack-dev-server": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz",
+ "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-html": "0.0.7",
+ "bonjour": "^3.5.0",
+ "chokidar": "^2.1.8",
+ "compression": "^1.7.4",
+ "connect-history-api-fallback": "^1.6.0",
+ "debug": "^4.1.1",
+ "del": "^4.1.1",
+ "express": "^4.17.1",
+ "html-entities": "^1.3.1",
+ "http-proxy-middleware": "0.19.1",
+ "import-local": "^2.0.0",
+ "internal-ip": "^4.3.0",
+ "ip": "^1.1.5",
+ "is-absolute-url": "^3.0.3",
+ "killable": "^1.0.1",
+ "loglevel": "^1.6.8",
+ "opn": "^5.5.0",
+ "p-retry": "^3.0.1",
+ "portfinder": "^1.0.26",
+ "schema-utils": "^1.0.0",
+ "selfsigned": "^1.10.7",
+ "semver": "^6.3.0",
+ "serve-index": "^1.9.1",
+ "sockjs": "0.3.20",
+ "sockjs-client": "1.4.0",
+ "spdy": "^4.0.2",
+ "strip-ansi": "^3.0.1",
+ "supports-color": "^6.1.0",
+ "url": "^0.11.0",
+ "webpack-dev-middleware": "^3.7.2",
+ "webpack-log": "^2.0.0",
+ "ws": "^6.2.1",
+ "yargs": "^13.3.2"
+ },
+ "bin": {
+ "webpack-dev-server": "bin/webpack-dev-server.js"
+ },
+ "engines": {
+ "node": ">= 6.11.5"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "dependencies": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/anymatch/node_modules/normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "dependencies": {
+ "remove-trailing-separator": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/braces/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/chokidar": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
+ "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "fsevents": "^1.2.7",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ },
+ "optionalDependencies": {
+ "fsevents": "^1.2.7"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/fill-range/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/glob-parent/node_modules/is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/is-absolute-url": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
+ "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-log": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
+ "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "^3.0.0",
+ "uuid": "^3.3.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz",
+ "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.15"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
+ "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==",
+ "dev": true,
+ "dependencies": {
+ "source-list-map": "^2.0.0",
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/webpack-sources/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-subresource-integrity": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.4.1.tgz",
+ "integrity": "sha512-XMLFInbGbB1HV7K4vHWANzc1CN0t/c4bBvnlvGxGwV45yE/S/feAXIm8dJsCkzqWtSKnmaEgTp/meyeThxG4Iw==",
+ "dev": true,
+ "dependencies": {
+ "webpack-sources": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/webpack/node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/braces/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/cacache": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz",
+ "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==",
+ "dev": true,
+ "dependencies": {
+ "bluebird": "^3.5.5",
+ "chownr": "^1.1.1",
+ "figgy-pudding": "^3.5.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.1.15",
+ "infer-owner": "^1.0.3",
+ "lru-cache": "^5.1.1",
+ "mississippi": "^3.0.0",
+ "mkdirp": "^0.5.1",
+ "move-concurrently": "^1.0.1",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^2.6.3",
+ "ssri": "^6.0.1",
+ "unique-filename": "^1.1.1",
+ "y18n": "^4.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true
+ },
+ "node_modules/webpack/node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/fill-range/node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/find-cache-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
+ "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
+ "dev": true,
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^2.0.0",
+ "pkg-dir": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/webpack/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/is-wsl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/webpack/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/webpack/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/memory-fs": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+ "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+ "dev": true,
+ "dependencies": {
+ "errno": "^0.1.3",
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "node_modules/webpack/node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/webpack/node_modules/schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/webpack/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack/node_modules/ssri": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
+ "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
+ "dev": true,
+ "dependencies": {
+ "figgy-pudding": "^3.5.1"
+ }
+ },
+ "node_modules/webpack/node_modules/terser-webpack-plugin": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
+ "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
+ "dev": true,
+ "dependencies": {
+ "cacache": "^12.0.2",
+ "find-cache-dir": "^2.1.0",
+ "is-wsl": "^1.1.0",
+ "schema-utils": "^1.0.0",
+ "serialize-javascript": "^4.0.0",
+ "source-map": "^0.6.1",
+ "terser": "^4.1.2",
+ "webpack-sources": "^1.4.0",
+ "worker-farm": "^1.7.0"
+ },
+ "engines": {
+ "node": ">= 6.9.0"
+ }
+ },
+ "node_modules/webpack/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz",
+ "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=",
+ "dev": true,
+ "dependencies": {
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
+ "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.4.24"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
+ "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==",
+ "dev": true
+ },
+ "node_modules/whatwg-url": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz",
+ "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==",
+ "dev": true,
+ "dependencies": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^2.0.2",
+ "webidl-conversions": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/when": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz",
+ "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=",
+ "dev": true
+ },
+ "node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "node_modules/wide-align/node_modules/ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/wide-align/node_modules/string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "dependencies": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/wide-align/node_modules/strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/widest-line": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
+ "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/widest-line/node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/widest-line/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/widest-line/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/widest-line/node_modules/string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/widest-line/node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/worker-farm": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
+ "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
+ "dev": true,
+ "dependencies": {
+ "errno": "~0.1.7"
+ }
+ },
+ "node_modules/worker-plugin": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-4.0.3.tgz",
+ "integrity": "sha512-7hFDYWiKcE3yHZvemsoM9lZis/PzurHAEX1ej8PLCu818Rt6QqUAiDdxHPCKZctzmhqzPpcFSgvMCiPbtooqAg==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^1.1.0"
+ }
+ },
+ "node_modules/worker-plugin/node_modules/json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/worker-plugin/node_modules/loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/ws": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
+ "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+ "devOptional": true,
+ "dependencies": {
+ "async-limiter": "~1.0.0"
+ }
+ },
+ "node_modules/xdg-basedir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/xhr2": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.0.tgz",
+ "integrity": "sha512-BDtiD0i2iKPK/S8OAZfpk6tyzEDnKKSjxWHcMBVmh+LuqJ8A32qXTyOx+TVOg2dKvq6zGBq2sgKPkEeRs1qTRA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
+ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
+ "dev": true
+ },
+ "node_modules/xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "dev": true,
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+ "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y-leveldb": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.0.tgz",
+ "integrity": "sha512-sMuitVrsAUNh+0b66I42nAuW3lCmez171uP4k0ePcTAJ+c+Iw9w4Yq3wwiyrDMFXBEyQSjSF86Inc23wEvWnxw==",
+ "optional": true,
+ "dependencies": {
+ "level": "^6.0.1",
+ "lib0": "^0.2.31"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ },
+ "peerDependencies": {
+ "yjs": "^13.0.0"
+ }
+ },
+ "node_modules/y-protocols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.3.tgz",
+ "integrity": "sha512-2hSl0dqrD8Kph0SpvyakVYpKEnTLOLGIf7yvwmloQ4qS6RSvl6fUYHy6YocCvTvcd9MBuNeO4EqlmBcONJsvtw==",
+ "dependencies": {
+ "lib0": "^0.2.35"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
+ "node_modules/y-websocket": {
+ "version": "1.3.11",
+ "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-1.3.11.tgz",
+ "integrity": "sha512-Cvf85SE1mwFxrMRCokr4Rj16febCtfJziQWGn/F74h2W37SGPPpPNQjYZR9PFG7ryMAskoMF3ge7ZR1IEnL5CQ==",
+ "dependencies": {
+ "lib0": "^0.2.35",
+ "lodash.debounce": "^4.0.8",
+ "ws": "^6.2.1",
+ "y-leveldb": "^0.1.0",
+ "y-protocols": "^1.0.3"
+ },
+ "bin": {
+ "y-websocket-server": "bin/server.js"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ },
+ "optionalDependencies": {
+ "ws": "^6.2.1",
+ "y-leveldb": "^0.1.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+ "dev": true
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
+ "node_modules/yeast": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
+ "dev": true
+ },
+ "node_modules/yjs": {
+ "version": "13.5.0",
+ "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.0.tgz",
+ "integrity": "sha512-fCBY8QIbQeXu8D6in4CBrdTCAmUsTHEgNXj27YnQDJMUQDNkXgvYV7vs1iiGekLoyBORt3/1qQa2cZqgvS8u8w==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "lib0": "^0.2.35"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/zone.js": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.3.tgz",
+ "integrity": "sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg=="
+ }
+ },
+ "dependencies": {
+ "@angular-devkit/architect": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1000.6.tgz",
+ "integrity": "sha512-IZ8yiiW+LQ5mI3VbNHzisTIn0j6D1inQZgcZtc5W2A7fFNvBlIh6vGU3mB6Qvg678Gt6tlvnNT6/R9A9Ct7VnA==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/core": "10.0.6",
+ "rxjs": "6.5.5"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "@angular-devkit/build-angular": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.1000.6.tgz",
+ "integrity": "sha512-tKyVD8Wqfo2wFdfWmc7OMzFn30Zl37heEusnMrQD5/zZ3Hw4Nqt2kf3pf3hbWl1GExUVFYyRNoCOCh9DaIfh0w==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/architect": "0.1000.6",
+ "@angular-devkit/build-optimizer": "0.1000.6",
+ "@angular-devkit/build-webpack": "0.1000.6",
+ "@angular-devkit/core": "10.0.6",
+ "@babel/core": "7.9.6",
+ "@babel/generator": "7.9.6",
+ "@babel/plugin-transform-runtime": "7.9.6",
+ "@babel/preset-env": "7.9.6",
+ "@babel/runtime": "7.9.6",
+ "@babel/template": "7.8.6",
+ "@jsdevtools/coverage-istanbul-loader": "3.0.3",
+ "@ngtools/webpack": "10.0.6",
+ "ajv": "6.12.3",
+ "autoprefixer": "9.8.0",
+ "babel-loader": "8.1.0",
+ "browserslist": "^4.9.1",
+ "cacache": "15.0.3",
+ "caniuse-lite": "^1.0.30001032",
+ "circular-dependency-plugin": "5.2.0",
+ "copy-webpack-plugin": "6.0.3",
+ "core-js": "3.6.4",
+ "css-loader": "3.5.3",
+ "cssnano": "4.1.10",
+ "file-loader": "6.0.0",
+ "find-cache-dir": "3.3.1",
+ "glob": "7.1.6",
+ "jest-worker": "26.0.0",
+ "karma-source-map-support": "1.4.0",
+ "less-loader": "6.1.0",
+ "license-webpack-plugin": "2.2.0",
+ "loader-utils": "2.0.0",
+ "mini-css-extract-plugin": "0.9.0",
+ "minimatch": "3.0.4",
+ "open": "7.0.4",
+ "parse5": "4.0.0",
+ "pnp-webpack-plugin": "1.6.4",
+ "postcss": "7.0.31",
+ "postcss-import": "12.0.1",
+ "postcss-loader": "3.0.0",
+ "raw-loader": "4.0.1",
+ "regenerator-runtime": "0.13.5",
+ "resolve-url-loader": "3.1.1",
+ "rimraf": "3.0.2",
+ "rollup": "2.10.9",
+ "rxjs": "6.5.5",
+ "sass": "1.26.5",
+ "sass-loader": "8.0.2",
+ "semver": "7.3.2",
+ "source-map": "0.7.3",
+ "source-map-loader": "1.0.0",
+ "source-map-support": "0.5.19",
+ "speed-measure-webpack-plugin": "1.3.3",
+ "style-loader": "1.2.1",
+ "stylus": "0.54.7",
+ "stylus-loader": "3.0.2",
+ "terser": "4.7.0",
+ "terser-webpack-plugin": "3.0.1",
+ "tree-kill": "1.2.2",
+ "webpack": "4.43.0",
+ "webpack-dev-middleware": "3.7.2",
+ "webpack-dev-server": "3.11.0",
+ "webpack-merge": "4.2.2",
+ "webpack-sources": "1.4.3",
+ "webpack-subresource-integrity": "1.4.1",
+ "worker-plugin": "4.0.3"
+ },
+ "dependencies": {
+ "core-js": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
+ "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==",
+ "dev": true
+ },
+ "parse5": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+ "dev": true
+ },
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "@angular-devkit/build-optimizer": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.1000.6.tgz",
+ "integrity": "sha512-R8zDEAvd9PeUKvOKh6I7xp3w+MViCwjGKoOZcznjH/i/9PQjOHCMwU5S48RQloQjMGu96eDMUGOVnd9qkzXUEw==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "2.0.0",
+ "source-map": "0.7.3",
+ "tslib": "2.0.0",
+ "webpack-sources": "1.4.3"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
+ "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==",
+ "dev": true
+ }
+ }
+ },
+ "@angular-devkit/build-webpack": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1000.6.tgz",
+ "integrity": "sha512-R01bJWuvckU5IdjcqoCeikLBpHRqt5fgfD0a4Hsg3evqW6xxXcSgc+YhWfeEmyU/nF/kVel8G2bFyPzhZP4QdQ==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/architect": "0.1000.6",
+ "@angular-devkit/core": "10.0.6",
+ "rxjs": "6.5.5"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "@angular-devkit/core": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.0.6.tgz",
+ "integrity": "sha512-mVvqSEoeErZ7bAModk95EAa6R9Nl23rvX+/TXuKVTK2dziMFBOrwHjb1DYhnZxFIH4xfUftCx+BWHjXBXCPYlA==",
+ "dev": true,
+ "requires": {
+ "ajv": "6.12.3",
+ "fast-json-stable-stringify": "2.1.0",
+ "magic-string": "0.25.7",
+ "rxjs": "6.5.5",
+ "source-map": "0.7.3"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "@angular-devkit/schematics": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-10.0.6.tgz",
+ "integrity": "sha512-V3T4cf+jVKiPYyBrSVHf3ZSnk4wIc1WEaaeFta56HccEGQCQpvAFKqDurmtMHer50Hhaxhn7IC3Oi5kPnvkNyQ==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/core": "10.0.6",
+ "ora": "4.0.4",
+ "rxjs": "6.5.5"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "@angular/animations": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-10.0.10.tgz",
+ "integrity": "sha512-lIbNeLVVl9bO41orPFpKoobCvxZIZ2wdcKJBEFtQiOdw0khRQQ8k7so4TAWOZXRJR+MkOUCjU2pO8gbMXgBweQ==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/cdk": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-10.1.3.tgz",
+ "integrity": "sha512-xMV1M41mfuaQod4rtAG/duYiWffGIC2C87E1YuyHTh8SEcHopGVRQd2C8PWH+iwinPbes7AjU1uzCEvmOYikrA==",
+ "requires": {
+ "parse5": "^5.0.0",
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/cli": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-10.0.6.tgz",
+ "integrity": "sha512-gQbQA/CsCyMf9RKEv1hJBCdBebV2BHeT4lGi56Eii0IkvZD5WIH0dNfQzR+6ErqGDgE1EI+9YCuX3psMEvCRUA==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/architect": "0.1000.6",
+ "@angular-devkit/core": "10.0.6",
+ "@angular-devkit/schematics": "10.0.6",
+ "@schematics/angular": "10.0.6",
+ "@schematics/update": "0.1000.6",
+ "@yarnpkg/lockfile": "1.1.0",
+ "ansi-colors": "4.1.1",
+ "debug": "4.1.1",
+ "ini": "1.3.5",
+ "inquirer": "7.1.0",
+ "npm-package-arg": "8.0.1",
+ "npm-pick-manifest": "6.1.0",
+ "open": "7.0.4",
+ "pacote": "9.5.12",
+ "read-package-tree": "5.3.1",
+ "rimraf": "3.0.2",
+ "semver": "7.3.2",
+ "symbol-observable": "1.2.0",
+ "universal-analytics": "0.4.20",
+ "uuid": "8.1.0"
+ },
+ "dependencies": {
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
+ "uuid": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
+ "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==",
+ "dev": true
+ }
+ }
+ },
+ "@angular/common": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/common/-/common-10.0.10.tgz",
+ "integrity": "sha512-p6/pTk0s0Ai5uUkOHHFZwp+TjxRNPldPxTU2LVxg2xuBEQTO53BsfBKn3zi74epdb1kBC0Yjdj6yEL4dITBs7A==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/compiler": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-10.0.10.tgz",
+ "integrity": "sha512-fO7kml0HUgnMa5eviKUk+j7NACASkoMAEgvbcVdKmGsSDu9YVkaqSdLXuj2vu9glSJWDRkZJKSrt9MzbmhyB5A==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/compiler-cli": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-10.0.10.tgz",
+ "integrity": "sha512-XkvWdJKr6HkyzAbcmy99HyDR4z949z9nHGwHNLBQjLbkX11i03fvS3bI5kgwqtNiLWYqxiPfXnpAyLBeFghCcw==",
+ "dev": true,
+ "requires": {
+ "canonical-path": "1.0.0",
+ "chokidar": "^3.0.0",
+ "convert-source-map": "^1.5.1",
+ "dependency-graph": "^0.7.2",
+ "fs-extra": "4.0.2",
+ "magic-string": "^0.25.0",
+ "minimist": "^1.2.0",
+ "reflect-metadata": "^0.1.2",
+ "semver": "^6.3.0",
+ "source-map": "^0.6.1",
+ "sourcemap-codec": "^1.4.8",
+ "tslib": "^2.0.0",
+ "yargs": "15.3.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "yargs": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz",
+ "integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "@angular/core": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-10.0.10.tgz",
+ "integrity": "sha512-PIQhLqjZayVXJoXs4WQu7orkePqFiux19y7bgBrsSAithe+g9BkrSIdX7+tkkX0zggUWKywY92YuMZCJ/S+uiw==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/forms": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-10.0.10.tgz",
+ "integrity": "sha512-bWjbsqMTiCNQZzXAfiEwT/tiAzSvChnqBimrJWNSHVYRkp71TkDcKXn6mA+E//YR0eZ84GKNNiVlKFxqkmeyqQ==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/material": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/@angular/material/-/material-10.1.3.tgz",
+ "integrity": "sha512-6ygbCVcejFydmZUlOcNreiWQTvL4kOrEp/M51DV70hqffTnxajCzaRe2MQhxisENB/bR8mtMvf8YY3Rsys/HCw==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/platform-browser": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-10.0.10.tgz",
+ "integrity": "sha512-srNGkvg9177skff7QOe3L+nGOSbrKLzFt3Z5O3oM0N0TWr8QlWEA+zQm8n0zLHI8AmdZbmFzAYYJiBvVCSc5RQ==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/platform-browser-dynamic": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-10.0.10.tgz",
+ "integrity": "sha512-6jbn0Ldyc+80BCETGtE7pzfKlbjfa/wEPhLEGWoYtxrrJ5UB3CblGpDMOsv1ibOQijPZ/JSmIMmAxz66+pLx3g==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@angular/platform-server": {
+ "version": "10.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-10.1.5.tgz",
+ "integrity": "sha512-n+6LEklqyzVdMiHRoGTU1MXECL/f6PdrLOJ8p5w5vak8dLQu83AHTO8SNC/YjrLanLgEXZXTG76AfGJbcMbiEw==",
+ "requires": {
+ "domino": "^2.1.2",
+ "tslib": "^2.0.0",
+ "xhr2": "^0.2.0"
+ }
+ },
+ "@angular/router": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@angular/router/-/router-10.0.10.tgz",
+ "integrity": "sha512-wDmr/Spuv4OhPK5a49AvgJhaedRw4yb7nmPMd51sWqzOV31RRcGXORjiXZOcSpElLxM9f7JV0tWDR5p5ko/kPA==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/compat-data": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz",
+ "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.12.0",
+ "invariant": "^2.2.4",
+ "semver": "^5.5.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "@babel/core": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz",
+ "integrity": "sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.8.3",
+ "@babel/generator": "^7.9.6",
+ "@babel/helper-module-transforms": "^7.9.0",
+ "@babel/helpers": "^7.9.6",
+ "@babel/parser": "^7.9.6",
+ "@babel/template": "^7.8.6",
+ "@babel/traverse": "^7.9.6",
+ "@babel/types": "^7.9.6",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.1",
+ "json5": "^2.1.2",
+ "lodash": "^4.17.13",
+ "resolve": "^1.3.2",
+ "semver": "^5.4.1",
+ "source-map": "^0.5.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ }
+ }
+ },
+ "@babel/generator": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz",
+ "integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.9.6",
+ "jsesc": "^2.5.1",
+ "lodash": "^4.17.13",
+ "source-map": "^0.5.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ }
+ }
+ },
+ "@babel/helper-annotate-as-pure": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz",
+ "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-builder-binary-assignment-operator-visitor": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz",
+ "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-explode-assignable-expression": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-compilation-targets": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz",
+ "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==",
+ "dev": true,
+ "requires": {
+ "@babel/compat-data": "^7.10.4",
+ "browserslist": "^4.12.0",
+ "invariant": "^2.2.4",
+ "levenary": "^1.1.1",
+ "semver": "^5.5.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "@babel/helper-create-regexp-features-plugin": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz",
+ "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-regex": "^7.10.4",
+ "regexpu-core": "^4.7.0"
+ }
+ },
+ "@babel/helper-define-map": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz",
+ "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/types": "^7.10.5",
+ "lodash": "^4.17.19"
+ }
+ },
+ "@babel/helper-explode-assignable-expression": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz",
+ "integrity": "sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A==",
+ "dev": true,
+ "requires": {
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-function-name": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz",
+ "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-get-function-arity": "^7.10.4",
+ "@babel/template": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ },
+ "dependencies": {
+ "@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ }
+ }
+ },
+ "@babel/helper-get-function-arity": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz",
+ "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-hoist-variables": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz",
+ "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-member-expression-to-functions": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz",
+ "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.11.0"
+ }
+ },
+ "@babel/helper-module-imports": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz",
+ "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-module-transforms": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz",
+ "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-imports": "^7.10.4",
+ "@babel/helper-replace-supers": "^7.10.4",
+ "@babel/helper-simple-access": "^7.10.4",
+ "@babel/helper-split-export-declaration": "^7.11.0",
+ "@babel/template": "^7.10.4",
+ "@babel/types": "^7.11.0",
+ "lodash": "^4.17.19"
+ },
+ "dependencies": {
+ "@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ }
+ }
+ },
+ "@babel/helper-optimise-call-expression": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz",
+ "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-plugin-utils": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
+ "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
+ "dev": true
+ },
+ "@babel/helper-regex": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz",
+ "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.19"
+ }
+ },
+ "@babel/helper-remap-async-to-generator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz",
+ "integrity": "sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-wrap-function": "^7.10.4",
+ "@babel/template": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ },
+ "dependencies": {
+ "@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ }
+ }
+ },
+ "@babel/helper-replace-supers": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz",
+ "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-member-expression-to-functions": "^7.10.4",
+ "@babel/helper-optimise-call-expression": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-simple-access": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz",
+ "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==",
+ "dev": true,
+ "requires": {
+ "@babel/template": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ },
+ "dependencies": {
+ "@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ }
+ }
+ },
+ "@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz",
+ "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.11.0"
+ }
+ },
+ "@babel/helper-split-export-declaration": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz",
+ "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.11.0"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
+ "dev": true
+ },
+ "@babel/helper-wrap-function": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz",
+ "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/template": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ },
+ "dependencies": {
+ "@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ }
+ }
+ },
+ "@babel/helpers": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz",
+ "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==",
+ "dev": true,
+ "requires": {
+ "@babel/template": "^7.10.4",
+ "@babel/traverse": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ },
+ "dependencies": {
+ "@babel/template": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
+ "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/parser": "^7.10.4",
+ "@babel/types": "^7.10.4"
+ }
+ }
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@babel/parser": {
+ "version": "7.11.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz",
+ "integrity": "sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==",
+ "dev": true
+ },
+ "@babel/plugin-proposal-async-generator-functions": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz",
+ "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-remap-async-to-generator": "^7.10.4",
+ "@babel/plugin-syntax-async-generators": "^7.8.0"
+ }
+ },
+ "@babel/plugin-proposal-dynamic-import": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz",
+ "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.0"
+ }
+ },
+ "@babel/plugin-proposal-json-strings": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz",
+ "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.0"
+ }
+ },
+ "@babel/plugin-proposal-nullish-coalescing-operator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz",
+ "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0"
+ }
+ },
+ "@babel/plugin-proposal-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+ }
+ },
+ "@babel/plugin-proposal-object-rest-spread": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz",
+ "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
+ "@babel/plugin-transform-parameters": "^7.10.4"
+ }
+ },
+ "@babel/plugin-proposal-optional-catch-binding": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz",
+ "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.0"
+ }
+ },
+ "@babel/plugin-proposal-optional-chaining": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz",
+ "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.0"
+ }
+ },
+ "@babel/plugin-proposal-unicode-property-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz",
+ "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-dynamic-import": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
+ "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-top-level-await": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz",
+ "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-arrow-functions": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz",
+ "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-async-to-generator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz",
+ "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-imports": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-remap-async-to-generator": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz",
+ "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-block-scoping": {
+ "version": "7.11.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz",
+ "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-classes": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz",
+ "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-define-map": "^7.10.4",
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/helper-optimise-call-expression": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-replace-supers": "^7.10.4",
+ "@babel/helper-split-export-declaration": "^7.10.4",
+ "globals": "^11.1.0"
+ }
+ },
+ "@babel/plugin-transform-computed-properties": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz",
+ "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-destructuring": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz",
+ "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-dotall-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz",
+ "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-duplicate-keys": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz",
+ "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz",
+ "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-for-of": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz",
+ "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-function-name": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz",
+ "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-literals": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz",
+ "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-member-expression-literals": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz",
+ "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-modules-amd": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz",
+ "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-transforms": "^7.10.5",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "babel-plugin-dynamic-import-node": "^2.3.3"
+ }
+ },
+ "@babel/plugin-transform-modules-commonjs": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz",
+ "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-transforms": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-simple-access": "^7.10.4",
+ "babel-plugin-dynamic-import-node": "^2.3.3"
+ }
+ },
+ "@babel/plugin-transform-modules-systemjs": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz",
+ "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-hoist-variables": "^7.10.4",
+ "@babel/helper-module-transforms": "^7.10.5",
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "babel-plugin-dynamic-import-node": "^2.3.3"
+ }
+ },
+ "@babel/plugin-transform-modules-umd": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz",
+ "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-transforms": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz",
+ "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-new-target": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz",
+ "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-object-super": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz",
+ "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-replace-supers": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-parameters": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz",
+ "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-get-function-arity": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-property-literals": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz",
+ "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-regenerator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz",
+ "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==",
+ "dev": true,
+ "requires": {
+ "regenerator-transform": "^0.14.2"
+ }
+ },
+ "@babel/plugin-transform-reserved-words": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz",
+ "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-runtime": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.6.tgz",
+ "integrity": "sha512-qcmiECD0mYOjOIt8YHNsAP1SxPooC/rDmfmiSK9BNY72EitdSc7l44WTEklaWuFtbOEBjNhWWyph/kOImbNJ4w==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-imports": "^7.8.3",
+ "@babel/helper-plugin-utils": "^7.8.3",
+ "resolve": "^1.8.1",
+ "semver": "^5.5.1"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "@babel/plugin-transform-shorthand-properties": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz",
+ "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-spread": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz",
+ "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0"
+ }
+ },
+ "@babel/plugin-transform-sticky-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz",
+ "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4",
+ "@babel/helper-regex": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-template-literals": {
+ "version": "7.10.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz",
+ "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-typeof-symbol": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz",
+ "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-transform-unicode-regex": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz",
+ "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-regexp-features-plugin": "^7.10.4",
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/preset-env": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.6.tgz",
+ "integrity": "sha512-0gQJ9RTzO0heXOhzftog+a/WyOuqMrAIugVYxMYf83gh1CQaQDjMtsOpqOwXyDL/5JcWsrCm8l4ju8QC97O7EQ==",
+ "dev": true,
+ "requires": {
+ "@babel/compat-data": "^7.9.6",
+ "@babel/helper-compilation-targets": "^7.9.6",
+ "@babel/helper-module-imports": "^7.8.3",
+ "@babel/helper-plugin-utils": "^7.8.3",
+ "@babel/plugin-proposal-async-generator-functions": "^7.8.3",
+ "@babel/plugin-proposal-dynamic-import": "^7.8.3",
+ "@babel/plugin-proposal-json-strings": "^7.8.3",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-proposal-numeric-separator": "^7.8.3",
+ "@babel/plugin-proposal-object-rest-spread": "^7.9.6",
+ "@babel/plugin-proposal-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-proposal-optional-chaining": "^7.9.0",
+ "@babel/plugin-proposal-unicode-property-regex": "^7.8.3",
+ "@babel/plugin-syntax-async-generators": "^7.8.0",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.0",
+ "@babel/plugin-syntax-json-strings": "^7.8.0",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.0",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.0",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.0",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3",
+ "@babel/plugin-transform-arrow-functions": "^7.8.3",
+ "@babel/plugin-transform-async-to-generator": "^7.8.3",
+ "@babel/plugin-transform-block-scoped-functions": "^7.8.3",
+ "@babel/plugin-transform-block-scoping": "^7.8.3",
+ "@babel/plugin-transform-classes": "^7.9.5",
+ "@babel/plugin-transform-computed-properties": "^7.8.3",
+ "@babel/plugin-transform-destructuring": "^7.9.5",
+ "@babel/plugin-transform-dotall-regex": "^7.8.3",
+ "@babel/plugin-transform-duplicate-keys": "^7.8.3",
+ "@babel/plugin-transform-exponentiation-operator": "^7.8.3",
+ "@babel/plugin-transform-for-of": "^7.9.0",
+ "@babel/plugin-transform-function-name": "^7.8.3",
+ "@babel/plugin-transform-literals": "^7.8.3",
+ "@babel/plugin-transform-member-expression-literals": "^7.8.3",
+ "@babel/plugin-transform-modules-amd": "^7.9.6",
+ "@babel/plugin-transform-modules-commonjs": "^7.9.6",
+ "@babel/plugin-transform-modules-systemjs": "^7.9.6",
+ "@babel/plugin-transform-modules-umd": "^7.9.0",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3",
+ "@babel/plugin-transform-new-target": "^7.8.3",
+ "@babel/plugin-transform-object-super": "^7.8.3",
+ "@babel/plugin-transform-parameters": "^7.9.5",
+ "@babel/plugin-transform-property-literals": "^7.8.3",
+ "@babel/plugin-transform-regenerator": "^7.8.7",
+ "@babel/plugin-transform-reserved-words": "^7.8.3",
+ "@babel/plugin-transform-shorthand-properties": "^7.8.3",
+ "@babel/plugin-transform-spread": "^7.8.3",
+ "@babel/plugin-transform-sticky-regex": "^7.8.3",
+ "@babel/plugin-transform-template-literals": "^7.8.3",
+ "@babel/plugin-transform-typeof-symbol": "^7.8.4",
+ "@babel/plugin-transform-unicode-regex": "^7.8.3",
+ "@babel/preset-modules": "^0.1.3",
+ "@babel/types": "^7.9.6",
+ "browserslist": "^4.11.1",
+ "core-js-compat": "^3.6.2",
+ "invariant": "^2.2.2",
+ "levenary": "^1.1.1",
+ "semver": "^5.5.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "@babel/preset-modules": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz",
+ "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
+ "@babel/plugin-transform-dotall-regex": "^7.4.4",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.9.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
+ "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@babel/template": {
+ "version": "7.8.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
+ "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.8.3",
+ "@babel/parser": "^7.8.6",
+ "@babel/types": "^7.8.6"
+ }
+ },
+ "@babel/traverse": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz",
+ "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/generator": "^7.11.0",
+ "@babel/helper-function-name": "^7.10.4",
+ "@babel/helper-split-export-declaration": "^7.11.0",
+ "@babel/parser": "^7.11.0",
+ "@babel/types": "^7.11.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0",
+ "lodash": "^4.17.19"
+ },
+ "dependencies": {
+ "@babel/generator": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz",
+ "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.11.0",
+ "jsesc": "^2.5.1",
+ "source-map": "^0.5.0"
+ }
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ }
+ }
+ },
+ "@babel/types": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
+ "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "lodash": "^4.17.19",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
+ "@bugsnag/browser": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@bugsnag/browser/-/browser-6.5.2.tgz",
+ "integrity": "sha512-XFKKorJc92ivLnlHHhLiPvkP03tZ5y7n0Z2xO6lOU7t+jWF5YapgwqQAda/TWvyYO38B/baWdnOpWMB3QmjhkA=="
+ },
+ "@bugsnag/js": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@bugsnag/js/-/js-6.5.2.tgz",
+ "integrity": "sha512-4ibw624fM5+Y/WSuo3T/MsJVtslsPV8X0MxFuRxdvpKVUXX216d8hN8E/bG4hr7aipqQOGhBYDqSzeL2wgmh0Q==",
+ "requires": {
+ "@bugsnag/browser": "^6.5.2",
+ "@bugsnag/node": "^6.5.2"
+ }
+ },
+ "@bugsnag/node": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@bugsnag/node/-/node-6.5.2.tgz",
+ "integrity": "sha512-KQ1twKoOttMCYsHv7OXUVsommVcrk6RGQ5YoZGlTbREhccbzsvjbiXPKiY31Qc7OXKvaJwSXhnOKrQTpRleFUg==",
+ "requires": {
+ "byline": "^5.0.0",
+ "error-stack-parser": "^2.0.2",
+ "iserror": "^0.0.2",
+ "pump": "^3.0.0",
+ "stack-generator": "^2.0.3"
+ }
+ },
+ "@istanbuljs/schema": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
+ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
+ "dev": true
+ },
+ "@jsdevtools/coverage-istanbul-loader": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.3.tgz",
+ "integrity": "sha512-TAdNkeGB5Fe4Og+ZkAr1Kvn9by2sfL44IAHFtxlh1BA1XJ5cLpO9iSNki5opWESv3l3vSHsZ9BNKuqFKbEbFaA==",
+ "dev": true,
+ "requires": {
+ "convert-source-map": "^1.7.0",
+ "istanbul-lib-instrument": "^4.0.1",
+ "loader-utils": "^1.4.0",
+ "merge-source-map": "^1.1.0",
+ "schema-utils": "^2.6.4"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ }
+ }
+ },
+ "@mrmlnc/readdir-enhanced": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
+ "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==",
+ "dev": true,
+ "requires": {
+ "call-me-maybe": "^1.0.1",
+ "glob-to-regexp": "^0.3.0"
+ }
+ },
+ "@ng-toolkit/_utils": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/@ng-toolkit/_utils/-/_utils-8.0.4.tgz",
+ "integrity": "sha512-UhFtW5XWhmTgg0KtS5i1t2VBCoqhRRLw/JBP2Q3EKMHAvNiX8AqJ/O23fo3RMzJfhdOINXpTQcT2KORdi0m83A==",
+ "requires": {
+ "@angular-devkit/core": "^8.3.21",
+ "@angular-devkit/schematics": "^8.3.21",
+ "@bugsnag/js": "^6.5.0",
+ "@schematics/angular": "^8.3.21",
+ "js-yaml": "^3.13.1",
+ "outdent": "^0.7.0"
+ },
+ "dependencies": {
+ "@angular-devkit/core": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.29.tgz",
+ "integrity": "sha512-4jdja9QPwR6XG14ZSunyyOWT3nE2WtZC5IMDIBZADxujXvhzOU0n4oWpy6/JVHLUAxYNNgzLz+/LQORRWndcPg==",
+ "requires": {
+ "ajv": "6.12.3",
+ "fast-json-stable-stringify": "2.0.0",
+ "magic-string": "0.25.3",
+ "rxjs": "6.4.0",
+ "source-map": "0.7.3"
+ }
+ },
+ "@angular-devkit/schematics": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.29.tgz",
+ "integrity": "sha512-AFJ9EK0XbcNlO5Dm9vr0OlBo1Nw6AaFXPR+DmHGBdcDDHxqEmYYLWfT+JU/8U2YFIdgrtlwvdtf6UQ3V2jdz1g==",
+ "requires": {
+ "@angular-devkit/core": "8.3.29",
+ "rxjs": "6.4.0"
+ }
+ },
+ "@schematics/angular": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.29.tgz",
+ "integrity": "sha512-If+UhCsQzCgnQymiiF8dQRoic34+RgJ6rV0n4k7Tm4N2xNYJOG7ajjzKM7PIeafsF50FKnFP8dqaNGxCMyq5Ew==",
+ "requires": {
+ "@angular-devkit/core": "8.3.29",
+ "@angular-devkit/schematics": "8.3.29"
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+ },
+ "magic-string": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz",
+ "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==",
+ "requires": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "rxjs": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
+ "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ }
+ }
+ },
+ "@ng-toolkit/universal": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@ng-toolkit/universal/-/universal-8.1.0.tgz",
+ "integrity": "sha512-B6lt06A1kZmhD4dTEv3ztmtpT5jtb64WKrlmoStRlG8UfX/c5hB2knTgwWA9jEEXJyjuH+rS0fdC+DLU+csp1w==",
+ "requires": {
+ "@angular-devkit/core": "^8.3.3",
+ "@angular-devkit/schematics": "^8.3.3",
+ "@bugsnag/js": "^6.4.0",
+ "@ng-toolkit/_utils": "8.0.4",
+ "@nguniversal/express-engine": "^8.1.1",
+ "@schematics/angular": "^8.3.3",
+ "tslib": "^1.9.0"
+ },
+ "dependencies": {
+ "@angular-devkit/core": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.29.tgz",
+ "integrity": "sha512-4jdja9QPwR6XG14ZSunyyOWT3nE2WtZC5IMDIBZADxujXvhzOU0n4oWpy6/JVHLUAxYNNgzLz+/LQORRWndcPg==",
+ "requires": {
+ "ajv": "6.12.3",
+ "fast-json-stable-stringify": "2.0.0",
+ "magic-string": "0.25.3",
+ "rxjs": "6.4.0",
+ "source-map": "0.7.3"
+ }
+ },
+ "@angular-devkit/schematics": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.29.tgz",
+ "integrity": "sha512-AFJ9EK0XbcNlO5Dm9vr0OlBo1Nw6AaFXPR+DmHGBdcDDHxqEmYYLWfT+JU/8U2YFIdgrtlwvdtf6UQ3V2jdz1g==",
+ "requires": {
+ "@angular-devkit/core": "8.3.29",
+ "rxjs": "6.4.0"
+ }
+ },
+ "@nguniversal/express-engine": {
+ "version": "8.2.6",
+ "resolved": "https://registry.npmjs.org/@nguniversal/express-engine/-/express-engine-8.2.6.tgz",
+ "integrity": "sha512-IKUKTpesgjYyB0Xg+fFhSbwbGBJhG0Wfn8MkQAi9RgSi8QsrSMkI3oUXc86Z7fpQL55D/ZIH7PekoC0Fmh/kxA=="
+ },
+ "@schematics/angular": {
+ "version": "8.3.29",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.29.tgz",
+ "integrity": "sha512-If+UhCsQzCgnQymiiF8dQRoic34+RgJ6rV0n4k7Tm4N2xNYJOG7ajjzKM7PIeafsF50FKnFP8dqaNGxCMyq5Ew==",
+ "requires": {
+ "@angular-devkit/core": "8.3.29",
+ "@angular-devkit/schematics": "8.3.29"
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+ },
+ "magic-string": {
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz",
+ "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==",
+ "requires": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "rxjs": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
+ "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ }
+ }
+ },
+ "@ngtools/webpack": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.0.6.tgz",
+ "integrity": "sha512-AbSDhPmsljkZO2jHFpge/5AHLQIrbscWgo4brrhF7NQ5TvPgE0Xn0wU7gxB9++hVUKQLGnnbAvewJyB/uYb9Nw==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/core": "10.0.6",
+ "enhanced-resolve": "4.1.1",
+ "rxjs": "6.5.5",
+ "webpack-sources": "1.4.3"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "@nguniversal/builders": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@nguniversal/builders/-/builders-10.1.0.tgz",
+ "integrity": "sha512-4GeQ9S7fVMRbj5bwjCE9VVstrYW3MFrqyIwFcbI/l5Oq1kzWFQ3B6hDX1CVEKQYiofgIi1OWDWAhr/ryrQj1yg==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/architect": "^0.1001.0",
+ "@angular-devkit/core": "^10.1.0",
+ "browser-sync": "^2.26.7",
+ "guess-parser": "^0.4.12",
+ "http-proxy-middleware": "^1.0.0",
+ "rxjs": "^6.5.5",
+ "tree-kill": "^1.2.1"
+ },
+ "dependencies": {
+ "@angular-devkit/architect": {
+ "version": "0.1001.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1001.6.tgz",
+ "integrity": "sha512-Wy10cGRdZ/g+akXbWfv0sq/pjVJrhrilSChe03ovu8nOsbcyZp76z+rnqf3YBYN6yZpWaBB80cW4QC/ar7Kv4Q==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/core": "10.1.6",
+ "rxjs": "6.6.2"
+ }
+ },
+ "@angular-devkit/core": {
+ "version": "10.1.6",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.6.tgz",
+ "integrity": "sha512-RhZCbX2I+ukR6/yu1OxwtyveBkQy+knRSQ7oxsBbwkS4M0XzmUswlf0p8lTfJI9pxrJnc2SODatMfEKeOYWmkA==",
+ "dev": true,
+ "requires": {
+ "ajv": "6.12.4",
+ "fast-json-stable-stringify": "2.1.0",
+ "magic-string": "0.25.7",
+ "rxjs": "6.6.2",
+ "source-map": "0.7.3"
+ }
+ },
+ "ajv": {
+ "version": "6.12.4",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
+ "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "http-proxy-middleware": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz",
+ "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==",
+ "dev": true,
+ "requires": {
+ "@types/http-proxy": "^1.17.4",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "lodash": "^4.17.20",
+ "micromatch": "^4.0.2"
+ }
+ }
+ }
+ },
+ "@nguniversal/common": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@nguniversal/common/-/common-10.1.0.tgz",
+ "integrity": "sha512-AIfLORs+LLHx9d+8kRNDq+GZj/2ToyXgg5Boi2RfgUhV5Rywey082XRlFmPwyVHxltYJzoMPeNWxzV6hrSMCzA==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@nguniversal/express-engine": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@nguniversal/express-engine/-/express-engine-10.1.0.tgz",
+ "integrity": "sha512-UYQB8662Qnx9Y2TblZmC8QbfAZtiCE6OeLNdwWIz8rVY9jhWi4P5SFb0slvcPMyPL5JAb+FHHOKjsH1NJztsCQ==",
+ "requires": {
+ "@nguniversal/common": "10.1.0",
+ "tslib": "^2.0.0"
+ }
+ },
+ "@ngx-utils/cookies": {
+ "version": "https://github.com/kenkeiras/ngx-cookies/releases/download/angular-10-support/ngx-cookies-angular-10.tgz",
+ "integrity": "sha512-4x7N5Yb2k364mBDqDgyRzxZOacDdS6yPpMvGTbgt21e4mY3NJFdb+MTjwno1wslt8wA4y8TEv8vM0ThO2pjBLQ=="
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
+ "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.3",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
+ "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
+ "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.3",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@npmcli/move-file": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz",
+ "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^1.0.4"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ }
+ }
+ },
+ "@schematics/angular": {
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-10.0.6.tgz",
+ "integrity": "sha512-TPBpo0GnMJLvKE6rYZDkSy9pnkMH55rSJ6nfLDpQ5zzmhoD/QnASUr8trfTFs3+MqmPlX61xI00+HmStmI8sJQ==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/core": "10.0.6",
+ "@angular-devkit/schematics": "10.0.6"
+ }
+ },
+ "@schematics/update": {
+ "version": "0.1000.6",
+ "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.1000.6.tgz",
+ "integrity": "sha512-GGfPGPjRF/MA4EeJ+h1ebzoYDzChF4BV7SaTfpT107LPCD3McRjKS39Jw2qH/ArGNSbrbJ8fYNOIj3g/uh1GoA==",
+ "dev": true,
+ "requires": {
+ "@angular-devkit/core": "10.0.6",
+ "@angular-devkit/schematics": "10.0.6",
+ "@yarnpkg/lockfile": "1.1.0",
+ "ini": "1.3.5",
+ "npm-package-arg": "^8.0.0",
+ "pacote": "9.5.12",
+ "rxjs": "6.5.5",
+ "semver": "7.3.2",
+ "semver-intersect": "1.4.0"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "@sindresorhus/is": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
+ "dev": true
+ },
+ "@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+ "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+ "dev": true,
+ "requires": {
+ "defer-to-connect": "^1.0.1"
+ }
+ },
+ "@types/body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/color-name": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+ "dev": true
+ },
+ "@types/connect": {
+ "version": "3.4.33",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
+ "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/cookie-parser": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz",
+ "integrity": "sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==",
+ "requires": {
+ "@types/express": "*"
+ }
+ },
+ "@types/express": {
+ "version": "4.17.8",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz",
+ "integrity": "sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==",
+ "requires": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "*",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "@types/express-serve-static-core": {
+ "version": "4.17.13",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz",
+ "integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==",
+ "requires": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*"
+ }
+ },
+ "@types/glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==",
+ "dev": true,
+ "requires": {
+ "@types/minimatch": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/http-proxy": {
+ "version": "1.17.4",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
+ "integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/jasmine": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.12.tgz",
+ "integrity": "sha512-vJaQ58oceFao+NzpKNqLOWwHPsqA7YEhKv+mOXvYU4/qh+BfVWIxaBtL0Ck5iCS67yOkNwGkDCrzepnzIWF+7g==",
+ "dev": true
+ },
+ "@types/json-schema": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
+ "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
+ "dev": true
+ },
+ "@types/mime": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
+ "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q=="
+ },
+ "@types/minimatch": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "11.15.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.20.tgz",
+ "integrity": "sha512-DY2QwdrBqNlsxdMehwzUtSsWHgYYPLVCAuXvOcu3wkzYmchbRunQ7OEZFOrmFoBLfA1ysz2Ypr6vtNP9WQkUaQ=="
+ },
+ "@types/q": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
+ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==",
+ "dev": true
+ },
+ "@types/qs": {
+ "version": "6.9.5",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
+ "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
+ },
+ "@types/range-parser": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
+ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
+ },
+ "@types/selenium-webdriver": {
+ "version": "3.0.17",
+ "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz",
+ "integrity": "sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw==",
+ "dev": true
+ },
+ "@types/serve-static": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz",
+ "integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==",
+ "requires": {
+ "@types/express-serve-static-core": "*",
+ "@types/mime": "*"
+ }
+ },
+ "@types/source-list-map": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
+ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
+ "dev": true
+ },
+ "@types/webpack-sources": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz",
+ "integrity": "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/source-list-map": "*",
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "@webassemblyjs/ast": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
+ "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/helper-module-context": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/wast-parser": "1.9.0"
+ }
+ },
+ "@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz",
+ "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==",
+ "dev": true
+ },
+ "@webassemblyjs/helper-api-error": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz",
+ "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==",
+ "dev": true
+ },
+ "@webassemblyjs/helper-buffer": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz",
+ "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==",
+ "dev": true
+ },
+ "@webassemblyjs/helper-code-frame": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz",
+ "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/wast-printer": "1.9.0"
+ }
+ },
+ "@webassemblyjs/helper-fsm": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz",
+ "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==",
+ "dev": true
+ },
+ "@webassemblyjs/helper-module-context": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz",
+ "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0"
+ }
+ },
+ "@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz",
+ "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==",
+ "dev": true
+ },
+ "@webassemblyjs/helper-wasm-section": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz",
+ "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-buffer": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/wasm-gen": "1.9.0"
+ }
+ },
+ "@webassemblyjs/ieee754": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz",
+ "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==",
+ "dev": true,
+ "requires": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "@webassemblyjs/leb128": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz",
+ "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==",
+ "dev": true,
+ "requires": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "@webassemblyjs/utf8": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz",
+ "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==",
+ "dev": true
+ },
+ "@webassemblyjs/wasm-edit": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz",
+ "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-buffer": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/helper-wasm-section": "1.9.0",
+ "@webassemblyjs/wasm-gen": "1.9.0",
+ "@webassemblyjs/wasm-opt": "1.9.0",
+ "@webassemblyjs/wasm-parser": "1.9.0",
+ "@webassemblyjs/wast-printer": "1.9.0"
+ }
+ },
+ "@webassemblyjs/wasm-gen": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz",
+ "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/ieee754": "1.9.0",
+ "@webassemblyjs/leb128": "1.9.0",
+ "@webassemblyjs/utf8": "1.9.0"
+ }
+ },
+ "@webassemblyjs/wasm-opt": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz",
+ "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-buffer": "1.9.0",
+ "@webassemblyjs/wasm-gen": "1.9.0",
+ "@webassemblyjs/wasm-parser": "1.9.0"
+ }
+ },
+ "@webassemblyjs/wasm-parser": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz",
+ "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-api-error": "1.9.0",
+ "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
+ "@webassemblyjs/ieee754": "1.9.0",
+ "@webassemblyjs/leb128": "1.9.0",
+ "@webassemblyjs/utf8": "1.9.0"
+ }
+ },
+ "@webassemblyjs/wast-parser": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz",
+ "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/floating-point-hex-parser": "1.9.0",
+ "@webassemblyjs/helper-api-error": "1.9.0",
+ "@webassemblyjs/helper-code-frame": "1.9.0",
+ "@webassemblyjs/helper-fsm": "1.9.0",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "@webassemblyjs/wast-printer": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz",
+ "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==",
+ "dev": true,
+ "requires": {
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/wast-parser": "1.9.0",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "@wessberg/ts-evaluator": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@wessberg/ts-evaluator/-/ts-evaluator-0.0.26.tgz",
+ "integrity": "sha512-2ktA630RnL6cIF3mHhHwjexvpl/mlvMJWxwMDdL8s5lWLFdby/7VJ2h2iFxosQu/l2cejI2zjXOieCLnSXt6Qg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.1.0",
+ "jsdom": "^16.3.0",
+ "object-path": "^0.11.4",
+ "tslib": "^2.0.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true
+ },
+ "@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true
+ },
+ "@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+ "dev": true
+ },
+ "abab": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz",
+ "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==",
+ "dev": true
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "abstract-leveldown": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
+ "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==",
+ "optional": true,
+ "requires": {
+ "buffer": "^5.5.0",
+ "immediate": "^3.2.3",
+ "level-concat-iterator": "~2.0.0",
+ "level-supports": "~1.0.0",
+ "xtend": "~4.0.0"
+ },
+ "dependencies": {
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "optional": true,
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "immediate": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz",
+ "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==",
+ "optional": true
+ }
+ }
+ },
+ "accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "requires": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ }
+ },
+ "acorn": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+ "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
+ "dev": true
+ },
+ "acorn-globals": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+ "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.1.1",
+ "acorn-walk": "^7.1.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true
+ }
+ }
+ },
+ "acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "dev": true
+ },
+ "adjust-sourcemap-loader": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz",
+ "integrity": "sha512-4hFsTsn58+YjrU9qKzML2JSSDqKvN8mUGQ0nNIrfPi8hmIONT4L3uUaT6MKdMsZ9AjsU6D2xDkZxCkbQPxChrA==",
+ "dev": true,
+ "requires": {
+ "assert": "1.4.1",
+ "camelcase": "5.0.0",
+ "loader-utils": "1.2.3",
+ "object-path": "0.11.4",
+ "regex-parser": "2.2.10"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
+ "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
+ "dev": true
+ },
+ "emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true
+ },
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^2.0.0",
+ "json5": "^1.0.1"
+ }
+ },
+ "object-path": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz",
+ "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=",
+ "dev": true
+ }
+ }
+ },
+ "adm-zip": {
+ "version": "0.4.16",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
+ "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
+ "dev": true
+ },
+ "after": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
+ "dev": true
+ },
+ "agent-base": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
+ "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+ "dev": true,
+ "requires": {
+ "es6-promisify": "^5.0.0"
+ }
+ },
+ "agentkeepalive": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz",
+ "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==",
+ "dev": true,
+ "requires": {
+ "humanize-ms": "^1.2.1"
+ }
+ },
+ "aggregate-error": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
+ "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
+ "dev": true,
+ "requires": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ }
+ },
+ "ajv": {
+ "version": "6.12.3",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
+ "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ajv-errors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
+ "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
+ "dev": true
+ },
+ "ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true
+ },
+ "alphanum-sort": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
+ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
+ "dev": true
+ },
+ "amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+ "dev": true
+ },
+ "ansi-align": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz",
+ "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==",
+ "dev": true,
+ "requires": {
+ "string-width": "^3.0.0"
+ }
+ },
+ "ansi-colors": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
+ "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
+ "dev": true
+ },
+ "ansi-escapes": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
+ "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.11.0"
+ }
+ },
+ "ansi-html": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
+ "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+ "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "app-root-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
+ "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==",
+ "dev": true
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+ "dev": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+ "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+ "dev": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "aria-query": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz",
+ "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=",
+ "dev": true,
+ "requires": {
+ "ast-types-flow": "0.0.7",
+ "commander": "^2.11.0"
+ }
+ },
+ "arity-n": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz",
+ "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=",
+ "dev": true
+ },
+ "arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+ "dev": true
+ },
+ "arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+ "dev": true
+ },
+ "arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+ "dev": true
+ },
+ "array-flatten": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
+ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
+ "dev": true
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true
+ },
+ "array-uniq": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+ "dev": true
+ },
+ "array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "dev": true
+ },
+ "arraybuffer.slice": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+ "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
+ "dev": true
+ },
+ "arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+ "dev": true
+ },
+ "asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
+ "dev": true
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "asn1.js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ }
+ }
+ },
+ "assert": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+ "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+ "dev": true,
+ "requires": {
+ "util": "0.10.3"
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true
+ },
+ "assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+ "dev": true
+ },
+ "ast-types-flow": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
+ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=",
+ "dev": true
+ },
+ "async": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
+ "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "async-each": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+ "dev": true
+ },
+ "async-each-series": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz",
+ "integrity": "sha1-dhfBkXQB/Yykooqtzj266Yr+tDI=",
+ "dev": true
+ },
+ "async-limiter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
+ "devOptional": true
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "dev": true
+ },
+ "atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "dev": true
+ },
+ "autoprefixer": {
+ "version": "9.8.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz",
+ "integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.12.0",
+ "caniuse-lite": "^1.0.30001061",
+ "chalk": "^2.4.2",
+ "normalize-range": "^0.1.2",
+ "num2fraction": "^1.2.2",
+ "postcss": "^7.0.30",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+ "dev": true
+ },
+ "aws4": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
+ "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
+ "dev": true
+ },
+ "axios": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
+ "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
+ "dev": true,
+ "requires": {
+ "follow-redirects": "1.5.10",
+ "is-buffer": "^2.0.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+ "dev": true,
+ "requires": {
+ "debug": "=3.1.0"
+ }
+ },
+ "is-buffer": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "axobject-query": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz",
+ "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==",
+ "dev": true,
+ "requires": {
+ "ast-types-flow": "0.0.7"
+ }
+ },
+ "babel-loader": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz",
+ "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==",
+ "dev": true,
+ "requires": {
+ "find-cache-dir": "^2.1.0",
+ "loader-utils": "^1.4.0",
+ "mkdirp": "^0.5.3",
+ "pify": "^4.0.1",
+ "schema-utils": "^2.6.5"
+ },
+ "dependencies": {
+ "find-cache-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
+ "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
+ "dev": true,
+ "requires": {
+ "commondir": "^1.0.1",
+ "make-dir": "^2.0.0",
+ "pkg-dir": "^3.0.0"
+ }
+ },
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ }
+ }
+ },
+ "babel-plugin-dynamic-import-node": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
+ "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==",
+ "dev": true,
+ "requires": {
+ "object.assign": "^4.1.0"
+ }
+ },
+ "backo2": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+ "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "dev": true,
+ "requires": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "base64-arraybuffer": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+ "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
+ "dev": true
+ },
+ "base64-js": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
+ "devOptional": true
+ },
+ "base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "dev": true
+ },
+ "batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
+ "dev": true
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "dev": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "better-assert": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+ "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+ "dev": true,
+ "requires": {
+ "callsite": "1.0.0"
+ }
+ },
+ "big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
+ "dev": true
+ },
+ "blob": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+ "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
+ "dev": true
+ },
+ "blocking-proxy": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
+ "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true
+ },
+ "bn.js": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
+ "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==",
+ "dev": true
+ },
+ "body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+ "requires": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ },
+ "dependencies": {
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
+ "bonjour": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
+ "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+ "dev": true,
+ "requires": {
+ "array-flatten": "^2.1.0",
+ "deep-equal": "^1.0.1",
+ "dns-equal": "^1.0.0",
+ "dns-txt": "^2.0.2",
+ "multicast-dns": "^6.0.1",
+ "multicast-dns-service-types": "^1.1.0"
+ }
+ },
+ "boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
+ "dev": true
+ },
+ "bootstrap": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.2.tgz",
+ "integrity": "sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A=="
+ },
+ "boxen": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz",
+ "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==",
+ "dev": true,
+ "requires": {
+ "ansi-align": "^3.0.0",
+ "camelcase": "^5.3.1",
+ "chalk": "^3.0.0",
+ "cli-boxes": "^2.2.0",
+ "string-width": "^4.1.0",
+ "term-size": "^2.1.0",
+ "type-fest": "^0.8.1",
+ "widest-line": "^3.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true
+ }
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+ "dev": true
+ },
+ "browser-process-hrtime": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
+ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
+ "dev": true
+ },
+ "browser-sync": {
+ "version": "2.26.13",
+ "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.26.13.tgz",
+ "integrity": "sha512-JPYLTngIzI+Dzx+StSSlMtF+Q9yjdh58HW6bMFqkFXuzQkJL8FCvp4lozlS6BbECZcsM2Gmlgp0uhEjvl18X4w==",
+ "dev": true,
+ "requires": {
+ "browser-sync-client": "^2.26.13",
+ "browser-sync-ui": "^2.26.13",
+ "bs-recipes": "1.3.4",
+ "bs-snippet-injector": "^2.0.1",
+ "chokidar": "^3.4.1",
+ "connect": "3.6.6",
+ "connect-history-api-fallback": "^1",
+ "dev-ip": "^1.0.1",
+ "easy-extender": "^2.3.4",
+ "eazy-logger": "3.1.0",
+ "etag": "^1.8.1",
+ "fresh": "^0.5.2",
+ "fs-extra": "3.0.1",
+ "http-proxy": "^1.18.1",
+ "immutable": "^3",
+ "localtunnel": "^2.0.0",
+ "micromatch": "^4.0.2",
+ "opn": "5.3.0",
+ "portscanner": "2.1.1",
+ "qs": "6.2.3",
+ "raw-body": "^2.3.2",
+ "resp-modifier": "6.0.2",
+ "rx": "4.1.0",
+ "send": "0.16.2",
+ "serve-index": "1.9.1",
+ "serve-static": "1.13.2",
+ "server-destroy": "1.0.1",
+ "socket.io": "2.1.1",
+ "ua-parser-js": "^0.7.18",
+ "yargs": "^15.4.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "base64id": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+ "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
+ "dev": true
+ },
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+ "dev": true
+ },
+ "connect": {
+ "version": "3.6.6",
+ "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+ "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+ "dev": true,
+ "requires": {
+ "debug": "2.6.9",
+ "finalhandler": "1.1.0",
+ "parseurl": "~1.3.2",
+ "utils-merge": "1.0.1"
+ }
+ },
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+ "dev": true
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "engine.io": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
+ "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==",
+ "dev": true,
+ "requires": {
+ "accepts": "~1.3.4",
+ "base64id": "1.0.0",
+ "cookie": "0.3.1",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.0",
+ "ws": "~3.3.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "engine.io-client": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+ "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
+ "dev": true,
+ "requires": {
+ "component-emitter": "1.2.1",
+ "component-inherit": "0.0.3",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.1",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "ws": "~3.3.1",
+ "xmlhttprequest-ssl": "~1.5.4",
+ "yeast": "0.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "engine.io-parser": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+ "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+ "dev": true,
+ "requires": {
+ "after": "0.8.2",
+ "arraybuffer.slice": "~0.0.7",
+ "base64-arraybuffer": "0.1.5",
+ "blob": "0.0.5",
+ "has-binary2": "~1.0.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+ "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+ "dev": true,
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.1",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "statuses": "~1.3.1",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "fs-extra": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
+ "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^3.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "dev": true,
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "dependencies": {
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "dev": true
+ }
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-wsl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+ "dev": true
+ },
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+ "dev": true
+ },
+ "jsonfile": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
+ "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "mime": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "opn": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
+ "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==",
+ "dev": true,
+ "requires": {
+ "is-wsl": "^1.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "qs": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
+ "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=",
+ "dev": true
+ },
+ "send": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+ "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "dev": true,
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.6.2",
+ "mime": "1.4.1",
+ "ms": "2.0.0",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.0",
+ "statuses": "~1.4.0"
+ },
+ "dependencies": {
+ "statuses": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+ "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+ "dev": true
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "dev": true,
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.2",
+ "send": "0.16.2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+ "dev": true
+ },
+ "socket.io": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
+ "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
+ "dev": true,
+ "requires": {
+ "debug": "~3.1.0",
+ "engine.io": "~3.2.0",
+ "has-binary2": "~1.0.2",
+ "socket.io-adapter": "~1.1.0",
+ "socket.io-client": "2.1.1",
+ "socket.io-parser": "~3.2.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "socket.io-client": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
+ "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
+ "dev": true,
+ "requires": {
+ "backo2": "1.0.2",
+ "base64-arraybuffer": "0.1.5",
+ "component-bind": "1.0.0",
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "engine.io-client": "~3.2.0",
+ "has-binary2": "~1.0.2",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "object-component": "0.0.3",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "socket.io-parser": "~3.2.0",
+ "to-array": "0.1.4"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "socket.io-parser": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+ "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
+ "dev": true,
+ "requires": {
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "statuses": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+ "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "ws": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+ "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+ "dev": true,
+ "requires": {
+ "async-limiter": "~1.0.0",
+ "safe-buffer": "~5.1.0",
+ "ultron": "~1.1.0"
+ }
+ },
+ "yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "browser-sync-client": {
+ "version": "2.26.13",
+ "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.26.13.tgz",
+ "integrity": "sha512-p2VbZoYrpuDhkreq+/Sv1MkToHklh7T1OaIntDwpG6Iy2q/XkBcgwPcWjX+WwRNiZjN8MEehxIjEUh12LweLmQ==",
+ "dev": true,
+ "requires": {
+ "etag": "1.8.1",
+ "fresh": "0.5.2",
+ "mitt": "^1.1.3",
+ "rxjs": "^5.5.6"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "5.5.12",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz",
+ "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==",
+ "dev": true,
+ "requires": {
+ "symbol-observable": "1.0.1"
+ }
+ },
+ "symbol-observable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
+ "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=",
+ "dev": true
+ }
+ }
+ },
+ "browser-sync-ui": {
+ "version": "2.26.13",
+ "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.26.13.tgz",
+ "integrity": "sha512-6NJ/pCnhCnBMzaty1opWo7ipDmFAIk8U71JMQGKJxblCUaGfdsbF2shf6XNZSkXYia1yS0vwKu9LIOzpXqQZCA==",
+ "dev": true,
+ "requires": {
+ "async-each-series": "0.1.1",
+ "connect-history-api-fallback": "^1",
+ "immutable": "^3",
+ "server-destroy": "1.0.1",
+ "socket.io-client": "^2.0.4",
+ "stream-throttle": "^0.1.3"
+ }
+ },
+ "browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "dev": true,
+ "requires": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "dev": true,
+ "requires": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "browserify-rsa": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "randombytes": "^2.0.1"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ }
+ }
+ },
+ "browserify-sign": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
+ "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^5.1.1",
+ "browserify-rsa": "^4.0.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.3",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.5",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ }
+ }
+ },
+ "browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "dev": true,
+ "requires": {
+ "pako": "~1.0.5"
+ }
+ },
+ "browserslist": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz",
+ "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==",
+ "dev": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30001111",
+ "electron-to-chromium": "^1.3.523",
+ "escalade": "^3.0.2",
+ "node-releases": "^1.1.60"
+ }
+ },
+ "browserstack": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.6.0.tgz",
+ "integrity": "sha512-HJDJ0TSlmkwnt9RZ+v5gFpa1XZTBYTj0ywvLwJ3241J7vMw2jAsGNVhKHtmCOyg+VxeLZyaibO9UL71AsUeDIw==",
+ "dev": true,
+ "requires": {
+ "https-proxy-agent": "^2.2.1"
+ }
+ },
+ "bs-recipes": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz",
+ "integrity": "sha1-DS1NSKcYyMBEdp/cT4lZLci2lYU=",
+ "dev": true
+ },
+ "bs-snippet-injector": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/bs-snippet-injector/-/bs-snippet-injector-2.0.1.tgz",
+ "integrity": "sha1-YbU5PxH1JVntEgaTEANDtu2wTdU=",
+ "dev": true
+ },
+ "buffer": {
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+ "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+ "dev": true,
+ "requires": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4",
+ "isarray": "^1.0.0"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "buffer-indexof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
+ "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
+ "dev": true
+ },
+ "buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+ "dev": true
+ },
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true
+ },
+ "builtin-status-codes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+ "dev": true
+ },
+ "builtins": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
+ "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=",
+ "dev": true
+ },
+ "byline": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
+ "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE="
+ },
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+ "dev": true
+ },
+ "cacache": {
+ "version": "15.0.3",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.3.tgz",
+ "integrity": "sha512-bc3jKYjqv7k4pWh7I/ixIjfcjPul4V4jme/WbjvwGS5LzoPL/GzXr4C5EgPNLO/QEZl9Oi61iGitYEdwcrwLCQ==",
+ "dev": true,
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^5.1.1",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "move-file": "^2.0.0",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.0",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "tar": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz",
+ "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==",
+ "dev": true,
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ }
+ }
+ }
+ },
+ "cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "dev": true,
+ "requires": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ }
+ },
+ "cacheable-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+ "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "dev": true,
+ "requires": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "http-cache-semantics": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
+ "dev": true
+ },
+ "lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "dev": true
+ },
+ "normalize-url": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
+ "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
+ "dev": true
+ }
+ }
+ },
+ "call-me-maybe": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+ "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
+ "dev": true
+ },
+ "caller-callsite": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
+ "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
+ "dev": true,
+ "requires": {
+ "callsites": "^2.0.0"
+ }
+ },
+ "caller-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
+ "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
+ "dev": true,
+ "requires": {
+ "caller-callsite": "^2.0.0"
+ }
+ },
+ "callsite": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+ "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
+ "dev": true
+ },
+ "callsites": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
+ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "caniuse-api": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
+ "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.0.0",
+ "caniuse-lite": "^1.0.0",
+ "lodash.memoize": "^4.1.2",
+ "lodash.uniq": "^4.5.0"
+ }
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001116",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz",
+ "integrity": "sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==",
+ "dev": true
+ },
+ "canonical-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz",
+ "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==",
+ "dev": true
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "chokidar": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
+ "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.1.2",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.4.0"
+ }
+ },
+ "chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true
+ },
+ "chrome-trace-event": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
+ "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
+ "dev": true
+ },
+ "cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "circular-dependency-plugin": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz",
+ "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==",
+ "dev": true
+ },
+ "class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "dev": true,
+ "requires": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ }
+ }
+ },
+ "clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true
+ },
+ "cli-boxes": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz",
+ "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "cli-spinners": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.4.0.tgz",
+ "integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==",
+ "dev": true
+ },
+ "cli-width": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
+ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
+ "dev": true
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+ "dev": true
+ },
+ "clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ }
+ },
+ "clone-response": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+ "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
+ "dev": true,
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "coa": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
+ "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
+ "dev": true,
+ "requires": {
+ "@types/q": "^1.5.1",
+ "chalk": "^2.4.1",
+ "q": "^1.1.2"
+ }
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+ "dev": true
+ },
+ "codelyzer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.0.tgz",
+ "integrity": "sha512-edJIQCIcxD9DhVSyBEdJ38AbLikm515Wl91t5RDGNT88uA6uQdTm4phTWfn9JhzAI8kXNUcfYyAE90lJElpGtA==",
+ "dev": true,
+ "requires": {
+ "@angular/compiler": "9.0.0",
+ "@angular/core": "9.0.0",
+ "app-root-path": "^3.0.0",
+ "aria-query": "^3.0.0",
+ "axobject-query": "2.0.2",
+ "css-selector-tokenizer": "^0.7.1",
+ "cssauron": "^1.4.0",
+ "damerau-levenshtein": "^1.0.4",
+ "rxjs": "^6.5.3",
+ "semver-dsl": "^1.0.1",
+ "source-map": "^0.5.7",
+ "sprintf-js": "^1.1.2",
+ "tslib": "^1.10.0",
+ "zone.js": "~0.10.3"
+ },
+ "dependencies": {
+ "@angular/compiler": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz",
+ "integrity": "sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==",
+ "dev": true
+ },
+ "@angular/core": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz",
+ "integrity": "sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ },
+ "sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
+ }
+ },
+ "collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "dev": true,
+ "requires": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ }
+ },
+ "color": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz",
+ "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.1",
+ "color-string": "^1.5.2"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "color-string": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
+ "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
+ "dev": true,
+ "requires": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
+ "dev": true
+ },
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+ "dev": true
+ },
+ "component-bind": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+ "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+ "dev": true
+ },
+ "component-emitter": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+ "dev": true
+ },
+ "component-inherit": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
+ "dev": true
+ },
+ "compose-function": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
+ "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=",
+ "dev": true,
+ "requires": {
+ "arity-n": "^1.0.4"
+ }
+ },
+ "compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dev": true,
+ "requires": {
+ "mime-db": ">= 1.43.0 < 2"
+ }
+ },
+ "compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "dev": true,
+ "requires": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "configstore": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+ "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+ "dev": true,
+ "requires": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ },
+ "dependencies": {
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "requires": {
+ "semver": "^6.0.0"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
+ "connect": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
+ "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
+ "dev": true,
+ "requires": {
+ "debug": "2.6.9",
+ "finalhandler": "1.1.2",
+ "parseurl": "~1.3.3",
+ "utils-merge": "1.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "connect-history-api-fallback": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
+ "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+ "dev": true
+ },
+ "console-browserify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
+ "dev": true
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+ "dev": true
+ },
+ "constants-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+ "dev": true
+ },
+ "content-disposition": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "convert-source-map": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
+ "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+ },
+ "cookie-parser": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
+ "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
+ "requires": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6"
+ }
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "copy-concurrently": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
+ "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
+ "dev": true,
+ "requires": {
+ "aproba": "^1.1.1",
+ "fs-write-stream-atomic": "^1.0.8",
+ "iferr": "^0.1.5",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.4",
+ "run-queue": "^1.0.0"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+ "dev": true
+ },
+ "copy-webpack-plugin": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.0.3.tgz",
+ "integrity": "sha512-q5m6Vz4elsuyVEIUXr7wJdIdePWTubsqVbEMvf1WQnHGv0Q+9yPRu7MtYFPt+GBOXRav9lvIINifTQ1vSCs+eA==",
+ "dev": true,
+ "requires": {
+ "cacache": "^15.0.4",
+ "fast-glob": "^3.2.4",
+ "find-cache-dir": "^3.3.1",
+ "glob-parent": "^5.1.1",
+ "globby": "^11.0.1",
+ "loader-utils": "^2.0.0",
+ "normalize-path": "^3.0.0",
+ "p-limit": "^3.0.1",
+ "schema-utils": "^2.7.0",
+ "serialize-javascript": "^4.0.0",
+ "webpack-sources": "^1.4.3"
+ },
+ "dependencies": {
+ "cacache": {
+ "version": "15.0.5",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz",
+ "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==",
+ "dev": true,
+ "requires": {
+ "@npmcli/move-file": "^1.0.1",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.0",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ }
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz",
+ "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "tar": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz",
+ "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==",
+ "dev": true,
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ }
+ }
+ }
+ },
+ "core-js": {
+ "version": "2.6.11",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
+ "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
+ },
+ "core-js-compat": {
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz",
+ "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.8.5",
+ "semver": "7.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
+ "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
+ "dev": true
+ }
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+ "dev": true
+ },
+ "cosmiconfig": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
+ "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
+ "dev": true,
+ "requires": {
+ "import-fresh": "^2.0.0",
+ "is-directory": "^0.3.1",
+ "js-yaml": "^3.13.1",
+ "parse-json": "^4.0.0"
+ }
+ },
+ "create-ecdh": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
+ "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ }
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "dev": true,
+ "requires": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ }
+ },
+ "crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+ "dev": true
+ },
+ "css": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
+ "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "source-map": "^0.6.1",
+ "source-map-resolve": "^0.5.2",
+ "urix": "^0.1.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "css-color-names": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
+ "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
+ "dev": true
+ },
+ "css-declaration-sorter": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz",
+ "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.1",
+ "timsort": "^0.3.0"
+ }
+ },
+ "css-loader": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.5.3.tgz",
+ "integrity": "sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "cssesc": "^3.0.0",
+ "icss-utils": "^4.1.1",
+ "loader-utils": "^1.2.3",
+ "normalize-path": "^3.0.0",
+ "postcss": "^7.0.27",
+ "postcss-modules-extract-imports": "^2.0.0",
+ "postcss-modules-local-by-default": "^3.0.2",
+ "postcss-modules-scope": "^2.2.0",
+ "postcss-modules-values": "^3.0.0",
+ "postcss-value-parser": "^4.0.3",
+ "schema-utils": "^2.6.6",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
+ "css-parse": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
+ "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
+ "dev": true,
+ "requires": {
+ "css": "^2.0.0"
+ }
+ },
+ "css-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
+ "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
+ "dev": true,
+ "requires": {
+ "boolbase": "^1.0.0",
+ "css-what": "^3.2.1",
+ "domutils": "^1.7.0",
+ "nth-check": "^1.0.2"
+ }
+ },
+ "css-select-base-adapter": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
+ "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
+ "dev": true
+ },
+ "css-selector-tokenizer": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz",
+ "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==",
+ "dev": true,
+ "requires": {
+ "cssesc": "^3.0.0",
+ "fastparse": "^1.1.2"
+ }
+ },
+ "css-tree": {
+ "version": "1.0.0-alpha.37",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
+ "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==",
+ "dev": true,
+ "requires": {
+ "mdn-data": "2.0.4",
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "css-what": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.3.0.tgz",
+ "integrity": "sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==",
+ "dev": true
+ },
+ "cssauron": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz",
+ "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=",
+ "dev": true,
+ "requires": {
+ "through": "X.X.X"
+ }
+ },
+ "cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true
+ },
+ "cssnano": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
+ "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
+ "dev": true,
+ "requires": {
+ "cosmiconfig": "^5.0.0",
+ "cssnano-preset-default": "^4.0.7",
+ "is-resolvable": "^1.0.0",
+ "postcss": "^7.0.0"
+ }
+ },
+ "cssnano-preset-default": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz",
+ "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==",
+ "dev": true,
+ "requires": {
+ "css-declaration-sorter": "^4.0.1",
+ "cssnano-util-raw-cache": "^4.0.1",
+ "postcss": "^7.0.0",
+ "postcss-calc": "^7.0.1",
+ "postcss-colormin": "^4.0.3",
+ "postcss-convert-values": "^4.0.1",
+ "postcss-discard-comments": "^4.0.2",
+ "postcss-discard-duplicates": "^4.0.2",
+ "postcss-discard-empty": "^4.0.1",
+ "postcss-discard-overridden": "^4.0.1",
+ "postcss-merge-longhand": "^4.0.11",
+ "postcss-merge-rules": "^4.0.3",
+ "postcss-minify-font-values": "^4.0.2",
+ "postcss-minify-gradients": "^4.0.2",
+ "postcss-minify-params": "^4.0.2",
+ "postcss-minify-selectors": "^4.0.2",
+ "postcss-normalize-charset": "^4.0.1",
+ "postcss-normalize-display-values": "^4.0.2",
+ "postcss-normalize-positions": "^4.0.2",
+ "postcss-normalize-repeat-style": "^4.0.2",
+ "postcss-normalize-string": "^4.0.2",
+ "postcss-normalize-timing-functions": "^4.0.2",
+ "postcss-normalize-unicode": "^4.0.1",
+ "postcss-normalize-url": "^4.0.1",
+ "postcss-normalize-whitespace": "^4.0.2",
+ "postcss-ordered-values": "^4.1.2",
+ "postcss-reduce-initial": "^4.0.3",
+ "postcss-reduce-transforms": "^4.0.2",
+ "postcss-svgo": "^4.0.2",
+ "postcss-unique-selectors": "^4.0.1"
+ }
+ },
+ "cssnano-util-get-arguments": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz",
+ "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=",
+ "dev": true
+ },
+ "cssnano-util-get-match": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz",
+ "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=",
+ "dev": true
+ },
+ "cssnano-util-raw-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz",
+ "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.0"
+ }
+ },
+ "cssnano-util-same-parent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz",
+ "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==",
+ "dev": true
+ },
+ "csso": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz",
+ "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==",
+ "dev": true,
+ "requires": {
+ "css-tree": "1.0.0-alpha.39"
},
"dependencies": {
- "loader-utils": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
- "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+ "css-tree": {
+ "version": "1.0.0-alpha.39",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz",
+ "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==",
"dev": true,
"requires": {
- "big.js": "^3.1.3",
- "emojis-list": "^2.0.0",
- "json5": "^0.5.0",
- "object-assign": "^4.0.1"
+ "mdn-data": "2.0.6",
+ "source-map": "^0.6.1"
}
},
+ "mdn-data": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz",
+ "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==",
+ "dev": true
+ },
"source-map": {
- "version": "0.1.43",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
- "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
- "dev": true,
- "requires": {
- "amdefine": ">=0.0.4"
- }
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
}
}
},
- "express": {
- "version": "4.17.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
- "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "cssom": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
+ "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==",
+ "dev": true
+ },
+ "cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
"dev": true,
"requires": {
- "accepts": "~1.3.7",
- "array-flatten": "1.1.1",
- "body-parser": "1.19.0",
- "content-disposition": "0.5.3",
- "content-type": "~1.0.4",
- "cookie": "0.4.0",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "~1.1.2",
- "fresh": "0.5.2",
- "merge-descriptors": "1.0.1",
- "methods": "~1.1.2",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.5",
- "qs": "6.7.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.1.2",
- "send": "0.17.1",
- "serve-static": "1.14.1",
- "setprototypeof": "1.1.1",
- "statuses": "~1.5.0",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
+ "cssom": "~0.3.6"
},
"dependencies": {
- "accepts": {
- "version": "1.3.7",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
- "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
- "dev": true,
- "requires": {
- "mime-types": "~2.1.24",
- "negotiator": "0.6.2"
- }
- },
- "array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
- "dev": true
- },
- "body-parser": {
- "version": "1.19.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
- "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
- "dev": true,
- "requires": {
- "bytes": "3.1.0",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "http-errors": "1.7.2",
- "iconv-lite": "0.4.24",
- "on-finished": "~2.3.0",
- "qs": "6.7.0",
- "raw-body": "2.4.0",
- "type-is": "~1.6.17"
- }
- },
- "bytes": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
- "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
- "dev": true
- },
- "cookie": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
- "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
- "dev": true
- },
- "http-errors": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
- "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
- "dev": true,
- "requires": {
- "depd": "~1.1.2",
- "inherits": "2.0.3",
- "setprototypeof": "1.1.1",
- "statuses": ">= 1.5.0 < 2",
- "toidentifier": "1.0.0"
- }
- },
- "iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- },
- "mime-db": {
- "version": "1.43.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
- "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
- "dev": true
- },
- "mime-types": {
- "version": "2.1.26",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
- "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
- "dev": true,
- "requires": {
- "mime-db": "1.43.0"
- }
- },
- "negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "dev": true
- },
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "dev": true
- },
- "qs": {
- "version": "6.7.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
- "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
- "dev": true
- },
- "range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "dev": true
- },
- "raw-body": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
- "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
- "dev": true,
- "requires": {
- "bytes": "3.1.0",
- "http-errors": "1.7.2",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- }
- },
- "setprototypeof": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
- "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
- "dev": true
- },
- "statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ }
+ }
+ },
+ "custom-event": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+ "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
+ "dev": true
+ },
+ "cyclist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
+ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
+ "dev": true
+ },
+ "d": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+ "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+ "dev": true,
+ "requires": {
+ "es5-ext": "^0.10.50",
+ "type": "^1.0.1"
+ }
+ },
+ "damerau-levenshtein": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
+ "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==",
+ "dev": true
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "data-urls": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
+ "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.3",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.0.0"
+ }
+ },
+ "date-format": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz",
+ "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "debuglog": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
+ "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=",
+ "dev": true
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "decimal.js": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
+ "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==",
+ "dev": true
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true
+ },
+ "decompress-response": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+ "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+ "dev": true,
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "deep-equal": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
+ "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
+ "dev": true,
+ "requires": {
+ "is-arguments": "^1.0.4",
+ "is-date-object": "^1.0.1",
+ "is-regex": "^1.0.4",
+ "object-is": "^1.0.1",
+ "object-keys": "^1.1.1",
+ "regexp.prototype.flags": "^1.2.0"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "default-gateway": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
+ "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==",
+ "dev": true,
+ "requires": {
+ "execa": "^1.0.0",
+ "ip-regex": "^2.1.0"
+ }
+ },
+ "defaults": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
+ "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+ "dev": true,
+ "requires": {
+ "clone": "^1.0.2"
+ },
+ "dependencies": {
+ "clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
- },
- "type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "dev": true,
- "requires": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- }
}
}
},
- "extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "defer-to-connect": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
"dev": true
},
- "extend-shallow": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
- "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
- "dev": true,
+ "deferred-leveldown": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz",
+ "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==",
+ "optional": true,
"requires": {
- "assign-symbols": "^1.0.0",
- "is-extendable": "^1.0.1"
- },
- "dependencies": {
- "is-extendable": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
- "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
- "dev": true,
- "requires": {
- "is-plain-object": "^2.0.4"
- }
- }
+ "abstract-leveldown": "~6.2.1",
+ "inherits": "^2.0.3"
}
},
- "external-editor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
- "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": {
- "chardet": "^0.7.0",
- "iconv-lite": "^0.4.24",
- "tmp": "^0.0.33"
- },
- "dependencies": {
- "iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- }
+ "object-keys": "^1.0.12"
}
},
- "extglob": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
- "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
"dev": true,
"requires": {
- "array-unique": "^0.3.2",
- "define-property": "^1.0.0",
- "expand-brackets": "^2.1.4",
- "extend-shallow": "^2.0.1",
- "fragment-cache": "^0.2.1",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.1"
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
},
"dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^1.0.0"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
"is-accessor-descriptor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
@@ -5926,933 +25970,1379 @@
}
}
},
- "extsprintf": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
- "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+ "del": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
+ "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
+ "dev": true,
+ "requires": {
+ "@types/glob": "^7.1.1",
+ "globby": "^6.1.0",
+ "is-path-cwd": "^2.0.0",
+ "is-path-in-cwd": "^2.0.0",
+ "p-map": "^2.0.0",
+ "pify": "^4.0.1",
+ "rimraf": "^2.6.3"
+ },
+ "dependencies": {
+ "array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+ "dev": true,
+ "requires": {
+ "array-uniq": "^1.0.1"
+ }
+ },
+ "globby": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+ "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+ "dev": true,
+ "requires": {
+ "array-union": "^1.0.1",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+ "dev": true
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "dependency-graph": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz",
+ "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==",
+ "dev": true
+ },
+ "des.js": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
+ "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "detect-node": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
+ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
+ "dev": true
+ },
+ "dev-ip": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz",
+ "integrity": "sha1-p2o+0YVb56ASu4rBbLgPPADcKPA=",
+ "dev": true
+ },
+ "dezalgo": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
+ "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
+ "dev": true,
+ "requires": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
+ "di": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+ "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
+ "dev": true
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ }
+ }
+ },
+ "dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ }
+ },
+ "dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true
+ },
+ "dns-equal": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
+ "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+ "dev": true
+ },
+ "dns-packet": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
+ "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+ "dev": true,
+ "requires": {
+ "ip": "^1.1.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "dns-txt": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
+ "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+ "dev": true,
+ "requires": {
+ "buffer-indexof": "^1.0.0"
+ }
+ },
+ "dom-serialize": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+ "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+ "dev": true,
+ "requires": {
+ "custom-event": "~1.0.0",
+ "ent": "~2.2.0",
+ "extend": "^3.0.0",
+ "void-elements": "^2.0.0"
+ }
+ },
+ "dom-serializer": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+ "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+ "dev": true,
+ "requires": {
+ "domelementtype": "^2.0.1",
+ "entities": "^2.0.0"
+ },
+ "dependencies": {
+ "domelementtype": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
+ "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
+ "dev": true
+ }
+ }
+ },
+ "domain-browser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
"dev": true
},
- "fast-deep-equal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
- "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+ "domelementtype": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"dev": true
},
- "fast-json-stable-stringify": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
- "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
- "dev": true
+ "domexception": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
+ "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
+ "dev": true,
+ "requires": {
+ "webidl-conversions": "^5.0.0"
+ }
},
- "fastparse": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
- "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
- "dev": true
+ "domino": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
+ "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ=="
},
- "faye-websocket": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
- "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+ "domutils": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+ "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
"dev": true,
"requires": {
- "websocket-driver": ">=0.5.1"
+ "dom-serializer": "0",
+ "domelementtype": "1"
}
},
- "figgy-pudding": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
- "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
+ "dot-prop": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
+ "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
+ "dev": true,
+ "requires": {
+ "is-obj": "^2.0.0"
+ }
+ },
+ "duplexer3": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
},
- "figures": {
+ "duplexify": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "easy-extender": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz",
+ "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.10"
+ }
+ },
+ "eazy-logger": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz",
- "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==",
+ "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-3.1.0.tgz",
+ "integrity": "sha512-/snsn2JqBtUSSstEl4R0RKjkisGHAhvYj89i7r3ytNUKW12y178KDZwXLXIgwDqLW6E/VRMT9qfld7wvFae8bQ==",
"dev": true,
"requires": {
- "escape-string-regexp": "^1.0.5"
+ "tfunk": "^4.0.0"
}
},
- "file-loader": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.2.0.tgz",
- "integrity": "sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ==",
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"dev": true,
"requires": {
- "loader-utils": "^1.2.3",
- "schema-utils": "^2.0.0"
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "electron-to-chromium": {
+ "version": "1.3.537",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.537.tgz",
+ "integrity": "sha512-v1jGX46P9vq1XvCBFJ7T7rHd2kMuSrCHnYvO0TqNoURYt7VoxCnqo5+W84s0jlnq0iQUPk5H2D01RfL4ENe2CA==",
+ "dev": true
+ },
+ "elliptic": {
+ "version": "6.5.3",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
+ "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.4.0",
+ "brorand": "^1.0.1",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.0"
},
"dependencies": {
- "ajv": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
- "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"dev": true
- },
- "schema-utils": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
- "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
- "dev": true,
- "requires": {
- "ajv": "^6.10.2",
- "ajv-keywords": "^3.4.1"
- }
}
}
},
- "fileset": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
- "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=",
- "dev": true,
- "requires": {
- "glob": "^7.0.3",
- "minimatch": "^3.0.3"
- }
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
},
- "fill-range": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
- "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "dev": true
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"requires": {
- "extend-shallow": "^2.0.1",
- "is-number": "^3.0.0",
- "repeat-string": "^1.6.1",
- "to-regex-range": "^2.1.0"
+ "iconv-lite": "^0.6.2"
},
"dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "iconv-lite": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
+ "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"dev": true,
"requires": {
- "is-extendable": "^0.1.0"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
},
- "finalhandler": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
- "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "encoding-down": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz",
+ "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==",
+ "optional": true,
+ "requires": {
+ "abstract-leveldown": "^6.2.1",
+ "inherits": "^2.0.3",
+ "level-codec": "^9.0.0",
+ "level-errors": "^2.0.0"
+ }
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "engine.io": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz",
+ "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==",
"dev": true,
"requires": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "statuses": "~1.5.0",
- "unpipe": "~1.0.0"
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "0.3.1",
+ "debug": "~4.1.0",
+ "engine.io-parser": "~2.2.0",
+ "ws": "^7.1.2"
},
"dependencies": {
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
"dev": true
},
- "statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "ws": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
+ "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
"dev": true
}
}
},
- "find-cache-dir": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz",
- "integrity": "sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==",
+ "engine.io-client": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz",
+ "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==",
"dev": true,
"requires": {
- "commondir": "^1.0.1",
- "make-dir": "^3.0.0",
- "pkg-dir": "^4.1.0"
- },
- "dependencies": {
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dev": true,
- "requires": {
- "p-locate": "^4.1.0"
- }
- },
- "make-dir": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz",
- "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==",
- "dev": true,
- "requires": {
- "semver": "^6.0.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true
- },
- "pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "component-emitter": "~1.3.0",
+ "component-inherit": "0.0.3",
+ "debug": "~4.1.0",
+ "engine.io-parser": "~2.2.0",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "ws": "~6.1.0",
+ "xmlhttprequest-ssl": "~1.5.4",
+ "yeast": "0.1.2"
+ },
+ "dependencies": {
+ "ws": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
+ "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
"dev": true,
"requires": {
- "find-up": "^4.0.0"
+ "async-limiter": "~1.0.0"
}
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
}
}
},
- "find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "engine.io-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz",
+ "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==",
"dev": true,
"requires": {
- "locate-path": "^3.0.0"
+ "after": "0.8.2",
+ "arraybuffer.slice": "~0.0.7",
+ "base64-arraybuffer": "0.1.5",
+ "blob": "0.0.5",
+ "has-binary2": "~1.0.2"
}
},
- "flatted": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
- "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
+ "enhanced-resolve": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz",
+ "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "memory-fs": "^0.5.0",
+ "tapable": "^1.0.0"
+ }
+ },
+ "ent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+ "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
"dev": true
},
- "flush-write-stream": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
- "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
+ "entities": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
+ "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
+ "dev": true
+ },
+ "err-code": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz",
+ "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=",
+ "dev": true
+ },
+ "errno": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+ "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+ "devOptional": true,
+ "requires": {
+ "prr": "~1.0.1"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"requires": {
- "inherits": "^2.0.3",
- "readable-stream": "^2.3.6"
+ "is-arrayish": "^0.2.1"
}
},
- "follow-redirects": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
- "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
+ "error-stack-parser": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
+ "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
+ "requires": {
+ "stackframe": "^1.1.1"
+ }
+ },
+ "es-abstract": {
+ "version": "1.17.6",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
+ "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
"dev": true,
"requires": {
- "debug": "^3.2.6"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
- }
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.0",
+ "is-regex": "^1.1.0",
+ "object-inspect": "^1.7.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.0",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
}
},
- "for-in": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
- "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
- "dev": true
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
},
- "forever-agent": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
- "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
- "dev": true
+ "es5-ext": {
+ "version": "0.10.53",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
+ "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
+ "dev": true,
+ "requires": {
+ "es6-iterator": "~2.0.3",
+ "es6-symbol": "~3.1.3",
+ "next-tick": "~1.0.0"
+ }
},
- "form-data": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
- "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"dev": true,
"requires": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12"
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
}
},
- "forwarded": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
- "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+ "es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"dev": true
},
- "fragment-cache": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
- "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "es6-promisify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+ "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"dev": true,
"requires": {
- "map-cache": "^0.2.2"
+ "es6-promise": "^4.0.3"
}
},
- "fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+ "es6-symbol": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+ "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+ "dev": true,
+ "requires": {
+ "d": "^1.0.1",
+ "ext": "^1.1.2"
+ }
+ },
+ "escalade": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz",
+ "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==",
"dev": true
},
- "from2": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
- "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+ "escape-goat": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
+ "dev": true
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "escodegen": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
+ "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"dev": true,
"requires": {
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.0"
+ "esprima": "^4.0.1",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true
+ }
}
},
- "fs-access": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
- "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
+ "eslint-scope": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+ "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
"dev": true,
"requires": {
- "null-check": "^1.0.0"
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
}
},
- "fs-extra": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
- "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ },
+ "esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
"dev": true,
"requires": {
- "graceful-fs": "^4.1.2",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
+ "estraverse": "^4.1.0"
}
},
- "fs-minipass": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
- "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
+ "estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "ev-emitter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz",
+ "integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q=="
+ },
+ "eventemitter3": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
+ "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==",
+ "dev": true
+ },
+ "events": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
+ "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==",
+ "dev": true
+ },
+ "eventsource": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
+ "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
"dev": true,
"requires": {
- "minipass": "^2.2.1"
+ "original": "^1.0.0"
}
},
- "fs-write-stream-atomic": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
- "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
+ "evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "dev": true,
+ "requires": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
"dev": true,
"requires": {
- "graceful-fs": "^4.1.2",
- "iferr": "^0.1.5",
- "imurmurhash": "^0.1.4",
- "readable-stream": "1 || 2"
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
}
},
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dev": true
},
- "fsevents": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz",
- "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==",
+ "expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
"dev": true,
- "optional": true,
"requires": {
- "nan": "^2.9.2",
- "node-pre-gyp": "^0.10.0"
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
},
"dependencies": {
- "abbrev": {
- "version": "1.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "ansi-regex": {
- "version": "2.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "aproba": {
- "version": "1.2.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "are-we-there-yet": {
- "version": "1.1.5",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "delegates": "^1.0.0",
- "readable-stream": "^2.0.6"
- }
- },
- "balanced-match": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "chownr": {
- "version": "1.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "code-point-at": {
- "version": "1.1.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "concat-map": {
- "version": "0.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "console-control-strings": {
- "version": "1.1.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "core-util-is": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
"debug": {
"version": "2.6.9",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
- "optional": true,
"requires": {
"ms": "2.0.0"
}
},
- "deep-extend": {
- "version": "0.6.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "delegates": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "detect-libc": {
- "version": "1.0.3",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "fs-minipass": {
- "version": "1.2.5",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "minipass": "^2.2.1"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "gauge": {
- "version": "2.7.4",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "aproba": "^1.0.3",
- "console-control-strings": "^1.0.0",
- "has-unicode": "^2.0.0",
- "object-assign": "^4.1.0",
- "signal-exit": "^3.0.0",
- "string-width": "^1.0.1",
- "strip-ansi": "^3.0.1",
- "wide-align": "^1.1.0"
- }
- },
- "glob": {
- "version": "7.1.3",
- "bundled": true,
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
"dev": true,
- "optional": true,
"requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "is-descriptor": "^0.1.0"
}
},
- "has-unicode": {
+ "extend-shallow": {
"version": "2.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "iconv-lite": {
- "version": "0.4.24",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- },
- "ignore-walk": {
- "version": "3.0.1",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "minimatch": "^3.0.4"
- }
- },
- "inflight": {
- "version": "1.0.6",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.3",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "ini": {
- "version": "1.3.5",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
- "optional": true,
"requires": {
- "number-is-nan": "^1.0.0"
+ "is-extendable": "^0.1.0"
}
},
- "isarray": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "exports-loader": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.6.3.tgz",
+ "integrity": "sha1-V9x4kX9wm5byR/qR5ptVTIVQE8g=",
+ "dev": true,
+ "requires": {
+ "loader-utils": "0.2.x",
+ "source-map": "0.1.x"
+ },
+ "dependencies": {
+ "big.js": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+ "dev": true
},
- "minimatch": {
- "version": "3.0.4",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
+ "emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true
},
- "minimist": {
- "version": "0.0.8",
- "bundled": true,
- "dev": true,
- "optional": true
+ "json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+ "dev": true
},
- "minipass": {
- "version": "2.3.5",
- "bundled": true,
+ "loader-utils": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+ "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
"dev": true,
- "optional": true,
"requires": {
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.0"
+ "big.js": "^3.1.3",
+ "emojis-list": "^2.0.0",
+ "json5": "^0.5.0",
+ "object-assign": "^4.0.1"
}
},
- "minizlib": {
- "version": "1.2.1",
- "bundled": true,
+ "source-map": {
+ "version": "0.1.43",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+ "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
"dev": true,
- "optional": true,
"requires": {
- "minipass": "^2.2.1"
+ "amdefine": ">=0.0.4"
}
+ }
+ }
+ },
+ "express": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "requires": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
- "mkdirp": {
- "version": "0.5.1",
- "bundled": true,
- "dev": true,
- "optional": true,
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
- "minimist": "0.0.8"
+ "ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "needle": {
- "version": "2.2.4",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
+ "ext": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
+ "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
+ "dev": true,
+ "requires": {
+ "type": "^2.0.0"
+ },
+ "dependencies": {
+ "type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==",
+ "dev": true
+ }
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
"dev": true,
- "optional": true,
"requires": {
- "debug": "^2.1.2",
- "iconv-lite": "^0.4.4",
- "sax": "^1.2.4"
+ "is-plain-object": "^2.0.4"
}
- },
- "node-pre-gyp": {
- "version": "0.10.3",
- "bundled": true,
+ }
+ }
+ },
+ "external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
- "optional": true,
"requires": {
- "detect-libc": "^1.0.2",
- "mkdirp": "^0.5.1",
- "needle": "^2.2.1",
- "nopt": "^4.0.1",
- "npm-packlist": "^1.1.6",
- "npmlog": "^4.0.2",
- "rc": "^1.2.7",
- "rimraf": "^2.6.1",
- "semver": "^5.3.0",
- "tar": "^4"
+ "safer-buffer": ">= 2.1.2 < 3"
}
- },
- "nopt": {
- "version": "4.0.1",
- "bundled": true,
+ }
+ }
+ },
+ "extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "dev": true,
+ "requires": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
"dev": true,
- "optional": true,
"requires": {
- "abbrev": "1",
- "osenv": "^0.1.4"
+ "is-descriptor": "^1.0.0"
}
},
- "npm-bundled": {
- "version": "1.0.5",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "npm-packlist": {
- "version": "1.2.0",
- "bundled": true,
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
- "optional": true,
"requires": {
- "ignore-walk": "^3.0.1",
- "npm-bundled": "^1.0.1"
+ "is-extendable": "^0.1.0"
}
},
- "npmlog": {
- "version": "4.1.2",
- "bundled": true,
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
"dev": true,
- "optional": true,
"requires": {
- "are-we-there-yet": "~1.1.2",
- "console-control-strings": "~1.1.0",
- "gauge": "~2.7.3",
- "set-blocking": "~2.0.0"
+ "kind-of": "^6.0.0"
}
},
- "number-is-nan": {
- "version": "1.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "object-assign": {
- "version": "4.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "once": {
- "version": "1.4.0",
- "bundled": true,
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
"dev": true,
- "optional": true,
"requires": {
- "wrappy": "1"
+ "kind-of": "^6.0.0"
}
},
- "os-homedir": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "os-tmpdir": {
+ "is-descriptor": {
"version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "osenv": {
- "version": "0.1.5",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "os-homedir": "^1.0.0",
- "os-tmpdir": "^1.0.0"
- }
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "process-nextick-args": {
- "version": "2.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "rc": {
- "version": "1.2.8",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "dependencies": {
- "minimist": {
- "version": "1.2.0",
- "bundled": true,
- "dev": true,
- "optional": true
- }
- }
- },
- "readable-stream": {
- "version": "2.3.6",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "rimraf": {
- "version": "2.6.3",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
"dev": true,
- "optional": true,
"requires": {
- "glob": "^7.1.3"
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "fast-glob": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz",
+ "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.0",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.2",
+ "picomatch": "^2.2.1"
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "fastparse": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
+ "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
+ "dev": true
+ },
+ "fastq": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz",
+ "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "faye-websocket": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+ "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+ "dev": true,
+ "requires": {
+ "websocket-driver": ">=0.5.1"
+ }
+ },
+ "figgy-pudding": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
+ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==",
+ "dev": true
+ },
+ "figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-loader": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.0.0.tgz",
+ "integrity": "sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.5"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
}
},
- "safe-buffer": {
- "version": "5.1.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "safer-buffer": {
- "version": "2.1.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "sax": {
- "version": "1.2.4",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "semver": {
- "version": "5.6.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "set-blocking": {
+ "ms": {
"version": "2.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "signal-exit": {
- "version": "3.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "string-width": {
- "version": "1.0.2",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
+ "find-cache-dir": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
+ "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==",
+ "dev": true,
+ "requires": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
- "optional": true,
"requires": {
- "code-point-at": "^1.0.0",
- "is-fullwidth-code-point": "^1.0.0",
- "strip-ansi": "^3.0.0"
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
}
},
- "string_decoder": {
- "version": "1.1.1",
- "bundled": true,
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
- "optional": true,
"requires": {
- "safe-buffer": "~5.1.0"
+ "p-locate": "^4.1.0"
}
},
- "strip-ansi": {
- "version": "3.0.1",
- "bundled": true,
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
- "optional": true,
"requires": {
- "ansi-regex": "^2.0.0"
+ "semver": "^6.0.0"
}
},
- "strip-json-comments": {
- "version": "2.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "tar": {
- "version": "4.4.8",
- "bundled": true,
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
- "optional": true,
"requires": {
- "chownr": "^1.1.1",
- "fs-minipass": "^1.2.5",
- "minipass": "^2.3.4",
- "minizlib": "^1.1.1",
- "mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.2"
+ "p-limit": "^2.2.0"
}
},
- "util-deprecate": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
},
- "wide-align": {
- "version": "1.1.3",
- "bundled": true,
+ "pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
- "optional": true,
"requires": {
- "string-width": "^1.0.2 || 2"
+ "find-up": "^4.0.0"
}
},
- "wrappy": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "yallist": {
- "version": "3.0.3",
- "bundled": true,
- "dev": true,
- "optional": true
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
}
}
},
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "flatted": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
+ "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
+ "dev": true
+ },
+ "flush-write-stream": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.3.6"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
+ "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==",
+ "dev": true
+ },
+ "for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+ "dev": true
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+ "dev": true
+ },
+ "form-data": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "dev": true,
+ "requires": {
+ "map-cache": "^0.2.2"
+ }
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "from2": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+ "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0"
+ }
+ },
+ "fs-extra": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz",
+ "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "fs-write-stream-atomic": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
+ "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "iferr": "^0.1.5",
+ "imurmurhash": "^0.1.4",
+ "readable-stream": "1 || 2"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+ "dev": true,
+ "optional": true
+ },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
+ "fuse.js": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-5.2.3.tgz",
+ "integrity": "sha512-ld3AEgKtKnnXCtJavtygAb+aLlD5aVvLwTocXXBSStLA6JGFI6oMxTvumwh46N2/3gs3A7JNDu1px5F1/cq84g=="
+ },
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -6867,6 +27357,28 @@
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
}
},
"genfun": {
@@ -6875,10 +27387,16 @@
"integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==",
"dev": true
},
+ "gensync": {
+ "version": "1.0.0-beta.1",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz",
+ "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==",
+ "dev": true
+ },
"get-caller-file": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
- "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"get-stream": {
@@ -6906,9 +27424,9 @@
}
},
"glob": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
- "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
@@ -6920,24 +27438,27 @@
}
},
"glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"dev": true,
"requires": {
- "is-glob": "^3.1.0",
- "path-dirname": "^1.0.0"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.0"
- }
- }
+ "is-glob": "^4.0.1"
+ }
+ },
+ "glob-to-regexp": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz",
+ "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
+ "dev": true
+ },
+ "global-dirs": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz",
+ "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==",
+ "dev": true,
+ "requires": {
+ "ini": "^1.3.5"
}
},
"globals": {
@@ -6947,25 +27468,17 @@
"dev": true
},
"globby": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
- "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz",
+ "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==",
"dev": true,
"requires": {
- "array-union": "^1.0.1",
- "dir-glob": "^2.0.0",
- "glob": "^7.1.2",
- "ignore": "^3.3.5",
- "pify": "^3.0.0",
- "slash": "^1.0.0"
- },
- "dependencies": {
- "pify": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
- "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
- }
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.1.1",
+ "ignore": "^5.1.4",
+ "merge2": "^1.3.0",
+ "slash": "^3.0.0"
}
},
"google-closure-library": {
@@ -6974,38 +27487,46 @@
"integrity": "sha512-B+Cdh2c3BbvSIONufK3yU/yKwhm7vxaqrAvxIBo3JmUAhA3WQPRSculbJPKC4ca7b/pjlsIR76KDpVqVrJd4dg==",
"dev": true
},
- "graceful-fs": {
- "version": "4.1.15",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
- "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
- "dev": true
+ "got": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+ "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+ "dev": true,
+ "requires": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ }
},
- "handle-thing": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
- "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==",
+ "graceful-fs": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
+ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true
},
- "handlebars": {
- "version": "4.7.3",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz",
- "integrity": "sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==",
+ "guess-parser": {
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/guess-parser/-/guess-parser-0.4.21.tgz",
+ "integrity": "sha512-DDrCBOx1g4KvamxwlLPA4bMdJWXEDSnRIFIUIllIhZ4hy2eOTQtn1DyVak7uUtsN9Zp11JUFNdDEnChjkRmFxg==",
"dev": true,
"requires": {
- "neo-async": "^2.6.0",
- "optimist": "^0.6.1",
- "source-map": "^0.6.1",
- "uglify-js": "^3.1.4"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- }
+ "@wessberg/ts-evaluator": "0.0.26"
}
},
+ "handle-thing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+ "dev": true
+ },
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -7013,12 +27534,12 @@
"dev": true
},
"har-validator": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
- "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
+ "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"dev": true,
"requires": {
- "ajv": "^6.5.5",
+ "ajv": "^6.12.3",
"har-schema": "^2.0.0"
}
},
@@ -7102,6 +27623,26 @@
"kind-of": "^4.0.0"
},
"dependencies": {
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
"kind-of": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
@@ -7113,14 +27654,40 @@
}
}
},
+ "has-yarn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
+ "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
+ "dev": true
+ },
"hash-base": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
- "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
+ "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"dev": true,
"requires": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ }
}
},
"hash.js": {
@@ -7151,28 +27718,22 @@
}
},
"hosted-git-info": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.2.tgz",
- "integrity": "sha512-ezZMWtHXm7Eb7Rq4Mwnx2vs79WUx2QmRg3+ZqeGroKzfDO+EprOcgRPYghsOP9JuYBfK18VojmRTGCg8Ma+ktw==",
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.5.tgz",
+ "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==",
"dev": true,
"requires": {
- "lru-cache": "^5.1.1"
+ "lru-cache": "^6.0.0"
},
"dependencies": {
"lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
- "yallist": "^3.0.2"
+ "yallist": "^4.0.0"
}
- },
- "yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
}
}
},
@@ -7206,10 +27767,25 @@
"integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
"dev": true
},
+ "html-encoding-sniffer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
+ "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
+ "dev": true,
+ "requires": {
+ "whatwg-encoding": "^1.0.5"
+ }
+ },
"html-entities": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
- "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz",
+ "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==",
+ "dev": true
+ },
+ "html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"http-cache-semantics": {
@@ -7225,30 +27801,31 @@
"dev": true
},
"http-errors": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
- "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
- "dev": true,
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
- "setprototypeof": "1.1.0",
- "statuses": ">= 1.4.0 < 2"
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ },
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ }
}
},
- "http-parser-js": {
- "version": "0.4.10",
- "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz",
- "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=",
- "dev": true
- },
"http-proxy": {
- "version": "1.17.0",
- "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
- "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"requires": {
- "eventemitter3": "^3.0.0",
+ "eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
@@ -7271,6 +27848,12 @@
"requires": {
"ms": "2.0.0"
}
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
}
}
},
@@ -7284,6 +27867,111 @@
"is-glob": "^4.0.0",
"lodash": "^4.17.11",
"micromatch": "^3.1.10"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ }
}
},
"http-signature": {
@@ -7313,15 +28001,6 @@
"debug": "^3.1.0"
},
"dependencies": {
- "agent-base": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
- "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
- "dev": true,
- "requires": {
- "es6-promisify": "^5.0.0"
- }
- },
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
@@ -7330,15 +28009,18 @@
"requires": {
"ms": "^2.1.1"
}
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
}
}
},
+ "huebee": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/huebee/-/huebee-2.1.0.tgz",
+ "integrity": "sha512-2im03Zw7MosL/h389ZwyMFv71JTglM4XvoahPRApajVthqBDS9Ro00zgTv6VKW5AXwZ83pNMDhCXC4TMluCSlg==",
+ "requires": {
+ "ev-emitter": "^1.1.1",
+ "unipointer": "^2.3.0"
+ }
+ },
"humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@@ -7349,19 +28031,28 @@
}
},
"iconv-lite": {
- "version": "0.4.23",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
- "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz",
+ "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
+ "icss-utils": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
+ "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.14"
+ }
+ },
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
- "dev": true
+ "devOptional": true
},
"iferr": {
"version": "0.1.5",
@@ -7370,9 +28061,15 @@
"dev": true
},
"ignore": {
- "version": "3.3.10",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
- "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+ "version": "5.1.8",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
+ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
+ "dev": true
+ },
+ "ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=",
"dev": true
},
"ignore-walk": {
@@ -7397,6 +28094,12 @@
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
"dev": true
},
+ "immutable": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+ "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=",
+ "dev": true
+ },
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -7425,6 +28128,12 @@
"resolve-from": "^3.0.0"
}
},
+ "import-lazy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+ "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
+ "dev": true
+ },
"import-local": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",
@@ -7445,6 +28154,24 @@
"source-map": "0.1.x"
},
"dependencies": {
+ "big.js": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+ "dev": true
+ },
+ "emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true
+ },
+ "json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+ "dev": true
+ },
"loader-utils": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
@@ -7509,10 +28236,10 @@
}
},
"inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "devOptional": true
},
"ini": {
"version": "1.3.5",
@@ -7521,23 +28248,23 @@
"dev": true
},
"inquirer": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz",
- "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
+ "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
"dev": true,
"requires": {
"ansi-escapes": "^4.2.1",
- "chalk": "^2.4.2",
+ "chalk": "^3.0.0",
"cli-cursor": "^3.1.0",
"cli-width": "^2.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.15",
"mute-stream": "0.0.8",
- "run-async": "^2.2.0",
- "rxjs": "^6.4.0",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.5.3",
"string-width": "^4.1.0",
- "strip-ansi": "^5.1.0",
+ "strip-ansi": "^6.0.0",
"through": "^2.3.6"
},
"dependencies": {
@@ -7547,18 +28274,59 @@
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
- "lodash": {
- "version": "4.17.15",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
- "dev": true
- },
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
@@ -7568,34 +28336,24 @@
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
- },
- "dependencies": {
- "strip-ansi": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
- "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
- "dev": true,
- "requires": {
- "ansi-regex": "^5.0.0"
- }
- }
}
},
"strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
- "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
"requires": {
- "ansi-regex": "^4.1.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
- "dev": true
- }
+ "has-flag": "^4.0.0"
}
}
}
@@ -7620,9 +28378,9 @@
}
},
"invert-kv": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
- "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
"dev": true
},
"ip": {
@@ -7638,10 +28396,9 @@
"dev": true
},
"ipaddr.js": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
- "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==",
- "dev": true
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"is-absolute-url": {
"version": "2.1.0",
@@ -7682,12 +28439,12 @@
"dev": true
},
"is-binary-path": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
- "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"requires": {
- "binary-extensions": "^1.0.0"
+ "binary-extensions": "^2.0.0"
}
},
"is-buffer": {
@@ -7697,11 +28454,20 @@
"dev": true
},
"is-callable": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
- "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
+ "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
"dev": true
},
+ "is-ci": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+ "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+ "dev": true,
+ "requires": {
+ "ci-info": "^2.0.0"
+ }
+ },
"is-color-stop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
@@ -7767,6 +28533,12 @@
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
"dev": true
},
+ "is-docker": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz",
+ "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==",
+ "dev": true
+ },
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -7780,21 +28552,36 @@
"dev": true
},
"is-fullwidth-code-point": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
- "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
- "number-is-nan": "^1.0.0"
+ "is-extglob": "^2.1.1"
}
},
- "is-glob": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
- "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+ "is-installed-globally": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz",
+ "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==",
"dev": true,
"requires": {
- "is-extglob": "^2.1.1"
+ "global-dirs": "^2.0.1",
+ "is-path-inside": "^3.0.1"
+ },
+ "dependencies": {
+ "is-path-inside": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
+ "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==",
+ "dev": true
+ }
}
},
"is-interactive": {
@@ -7803,54 +28590,55 @@
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"dev": true
},
+ "is-npm": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz",
+ "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==",
+ "dev": true
+ },
"is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-number-like": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz",
+ "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==",
"dev": true,
"requires": {
- "kind-of": "^3.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
+ "lodash.isfinite": "^3.3.2"
}
},
"is-obj": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
- "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"dev": true
},
"is-path-cwd": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
- "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+ "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
"dev": true
},
"is-path-in-cwd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
- "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+ "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
"dev": true,
"requires": {
- "is-path-inside": "^1.0.0"
+ "is-path-inside": "^2.1.0"
}
},
"is-path-inside": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
- "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+ "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
"dev": true,
"requires": {
- "path-is-inside": "^1.0.1"
+ "path-is-inside": "^1.0.2"
}
},
"is-plain-obj": {
@@ -7868,19 +28656,19 @@
"isobject": "^3.0.1"
}
},
- "is-promise": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
- "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+ "is-potential-custom-element-name": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
+ "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=",
"dev": true
},
"is-regex": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
- "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+ "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
- "has": "^1.0.3"
+ "has-symbols": "^1.0.1"
}
},
"is-resolvable": {
@@ -7926,9 +28714,18 @@
"dev": true
},
"is-wsl": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz",
- "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "requires": {
+ "is-docker": "^2.0.0"
+ }
+ },
+ "is-yarn-global": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
+ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
"dev": true
},
"isarray": {
@@ -7938,13 +28735,15 @@
"dev": true
},
"isbinaryfile": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
- "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==",
- "dev": true,
- "requires": {
- "buffer-alloc": "^1.2.0"
- }
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz",
+ "integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==",
+ "dev": true
+ },
+ "iserror": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/iserror/-/iserror-0.0.2.tgz",
+ "integrity": "sha1-vVNFH+L2aLnyQCwZZnh6qix8C/U="
},
"isexe": {
"version": "2.0.0",
@@ -7958,359 +28757,463 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
+ "isomorphic.js": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.5.tgz",
+ "integrity": "sha512-MkX5lLQApx/8IAIU31PKvpAZosnu2Jqcj1rM8TzxyA4CR96tv3SgMKQNTCxL58G7696Q57zd7ubHV/hTg+5fNA=="
+ },
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
- "istanbul-api": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.7.tgz",
- "integrity": "sha512-LYTOa2UrYFyJ/aSczZi/6lBykVMjCCvUmT64gOe+jPZFy4w6FYfPGqFT2IiQ2BxVHHDOvCD7qrIXb0EOh4uGWw==",
+ "istanbul-lib-coverage": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz",
+ "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==",
+ "dev": true
+ },
+ "istanbul-lib-instrument": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz",
+ "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==",
"dev": true,
"requires": {
- "async": "^2.6.2",
- "compare-versions": "^3.4.0",
- "fileset": "^2.0.3",
- "istanbul-lib-coverage": "^2.0.5",
- "istanbul-lib-hook": "^2.0.7",
- "istanbul-lib-instrument": "^3.3.0",
- "istanbul-lib-report": "^2.0.8",
- "istanbul-lib-source-maps": "^3.0.6",
- "istanbul-reports": "^2.2.5",
- "js-yaml": "^3.13.1",
- "make-dir": "^2.1.0",
- "minimatch": "^3.0.4",
- "once": "^1.4.0"
+ "@babel/core": "^7.7.5",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.0.0",
+ "semver": "^6.3.0"
},
"dependencies": {
- "@babel/generator": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz",
- "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.4.4",
- "jsesc": "^2.5.1",
- "lodash": "^4.17.11",
- "source-map": "^0.5.0",
- "trim-right": "^1.0.1"
- }
- },
- "@babel/helper-split-export-declaration": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
- "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.4.4"
- }
- },
- "@babel/parser": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz",
- "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==",
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
+ "istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+ "dev": true,
+ "requires": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^3.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
- "@babel/template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
- "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.0.0",
- "@babel/parser": "^7.4.4",
- "@babel/types": "^7.4.4"
- }
- },
- "@babel/traverse": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz",
- "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==",
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.0.0",
- "@babel/generator": "^7.4.4",
- "@babel/helper-function-name": "^7.1.0",
- "@babel/helper-split-export-declaration": "^7.4.4",
- "@babel/parser": "^7.4.4",
- "@babel/types": "^7.4.4",
- "debug": "^4.1.0",
- "globals": "^11.1.0",
- "lodash": "^4.17.11"
+ "semver": "^6.0.0"
}
},
- "@babel/types": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz",
- "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.11",
- "to-fast-properties": "^2.0.0"
- }
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
},
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
"requires": {
- "ms": "^2.1.1"
+ "has-flag": "^4.0.0"
}
- },
- "globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true
- },
+ }
+ }
+ },
+ "istanbul-lib-source-maps": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz",
+ "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==",
+ "dev": true,
+ "requires": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^2.0.5",
+ "make-dir": "^2.1.0",
+ "rimraf": "^2.6.3",
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
"istanbul-lib-coverage": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
"integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==",
"dev": true
},
- "istanbul-lib-hook": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz",
- "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==",
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
- "append-transform": "^1.0.0"
+ "glob": "^7.1.3"
}
},
- "istanbul-lib-instrument": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz",
- "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==",
- "dev": true,
- "requires": {
- "@babel/generator": "^7.4.0",
- "@babel/parser": "^7.4.3",
- "@babel/template": "^7.4.0",
- "@babel/traverse": "^7.4.3",
- "@babel/types": "^7.4.0",
- "istanbul-lib-coverage": "^2.0.5",
- "semver": "^6.0.0"
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "istanbul-reports": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz",
+ "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==",
+ "dev": true,
+ "requires": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ }
+ },
+ "jasmine": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.1.tgz",
+ "integrity": "sha512-Jqp8P6ZWkTVFGmJwBK46p+kJNrZCdqkQ4GL+PGuBXZwK1fM4ST9BizkYgIwCFqYYqnTizAy6+XG2Ej5dFrej9Q==",
+ "dev": true,
+ "requires": {
+ "fast-glob": "^2.2.6",
+ "jasmine-core": "~3.6.0"
+ },
+ "dependencies": {
+ "@nodelib/fs.stat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
+ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
+ "dev": true
+ },
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
}
},
- "istanbul-lib-report": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz",
- "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==",
+ "fast-glob": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
+ "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==",
"dev": true,
"requires": {
- "istanbul-lib-coverage": "^2.0.5",
- "make-dir": "^2.1.0",
- "supports-color": "^6.1.0"
+ "@mrmlnc/readdir-enhanced": "^2.2.1",
+ "@nodelib/fs.stat": "^1.1.2",
+ "glob-parent": "^3.1.0",
+ "is-glob": "^4.0.0",
+ "merge2": "^1.2.3",
+ "micromatch": "^3.1.10"
}
},
- "istanbul-lib-source-maps": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz",
- "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==",
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
"dev": true,
"requires": {
- "debug": "^4.1.1",
- "istanbul-lib-coverage": "^2.0.5",
- "make-dir": "^2.1.0",
- "rimraf": "^2.6.3",
- "source-map": "^0.6.1"
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
},
"dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
}
}
},
- "istanbul-reports": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.5.tgz",
- "integrity": "sha512-ilCSjE6f7elNIRxnSnIhnOpXdf3ryUT7Zkl+TaADItM638SWXjfNW40cujZCIjex4g4DTkzIy9kzwkaLruB50Q==",
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"dev": true,
"requires": {
- "handlebars": "^4.1.2"
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ }
}
},
- "jsesc": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
- "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
- "dev": true
- },
- "make-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
- "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"dev": true,
"requires": {
- "pify": "^4.0.1",
- "semver": "^5.6.0"
+ "kind-of": "^3.0.2"
},
"dependencies": {
- "semver": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
- "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
- "dev": true
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
}
}
},
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
- },
- "pify": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
- "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
- "dev": true
- },
- "semver": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
- "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==",
+ "jasmine-core": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz",
+ "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==",
"dev": true
},
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
},
- "to-fast-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
- "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
- "dev": true
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
}
}
},
- "istanbul-lib-coverage": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz",
- "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==",
+ "jasmine-core": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
+ "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
"dev": true
},
- "istanbul-lib-instrument": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz",
- "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==",
+ "jasmine-spec-reporter": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.2.tgz",
+ "integrity": "sha512-6gP1LbVgJ+d7PKksQBc2H0oDGNRQI3gKUsWlswKaQ2fif9X5gzhQcgM5+kiJGCQVurOG09jqNhk7payggyp5+g==",
"dev": true,
"requires": {
- "@babel/core": "^7.7.5",
- "@babel/parser": "^7.7.5",
- "@babel/template": "^7.7.4",
- "@babel/traverse": "^7.7.4",
- "@istanbuljs/schema": "^0.1.2",
- "istanbul-lib-coverage": "^3.0.0",
- "semver": "^6.3.0"
+ "colors": "1.4.0"
+ }
+ },
+ "jasmine-ts": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/jasmine-ts/-/jasmine-ts-0.3.0.tgz",
+ "integrity": "sha512-K5joodjVOh3bnD06CNXC8P5htDq/r0Rhjv66ECOpdIGFLly8kM7V+X/GXcd9kv+xO+tIq3q9Y8B5OF6yr/iiDw==",
+ "dev": true,
+ "requires": {
+ "yargs": "^8.0.2"
},
"dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+ "dev": true
+ },
+ "cliui": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+ "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
- "@babel/highlight": "^7.8.3"
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wrap-ansi": "^2.0.0"
+ },
+ "dependencies": {
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
}
},
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
+ "get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
+ "number-is-nan": "^1.0.0"
}
},
- "@babel/parser": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
- "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==",
+ "require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true
},
- "@babel/template": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
- "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==",
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.8.3",
- "@babel/parser": "^7.8.3",
- "@babel/types": "^7.8.3"
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
}
},
- "@babel/types": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
- "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
+ "wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
- "esutils": "^2.0.2",
- "lodash": "^4.17.13",
- "to-fast-properties": "^2.0.0"
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
}
},
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "y18n": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- }
- }
- },
- "jasmine": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz",
- "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=",
- "dev": true,
- "requires": {
- "exit": "^0.1.2",
- "glob": "^7.0.6",
- "jasmine-core": "~2.8.0"
- },
- "dependencies": {
- "jasmine-core": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz",
- "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
- "dev": true
+ "yargs": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz",
+ "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^4.1.0",
+ "cliui": "^3.2.0",
+ "decamelize": "^1.1.1",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^2.0.0",
+ "read-pkg-up": "^2.0.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^7.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz",
+ "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^4.1.0"
+ }
}
}
},
- "jasmine-core": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.3.0.tgz",
- "integrity": "sha512-3/xSmG/d35hf80BEN66Y6g9Ca5l/Isdeg/j6zvbTYlTzeKinzmaTM4p9am5kYqOmE05D7s1t8FGjzdSnbUbceA==",
- "dev": true
- },
- "jasmine-spec-reporter": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
- "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==",
- "dev": true,
- "requires": {
- "colors": "1.1.2"
- }
- },
"jasminewd2": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz",
@@ -8318,32 +29221,42 @@
"dev": true
},
"jest-worker": {
- "version": "24.9.0",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz",
- "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==",
+ "version": "26.0.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz",
+ "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==",
"dev": true,
"requires": {
"merge-stream": "^2.0.0",
- "supports-color": "^6.1.0"
+ "supports-color": "^7.0.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
}
},
- "js-levenshtein": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
- "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
- "dev": true
- },
"js-tokens": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
- "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"js-yaml": {
- "version": "3.13.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
- "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
- "dev": true,
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -8355,12 +29268,83 @@
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true
},
+ "jsdom": {
+ "version": "16.4.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz",
+ "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.3",
+ "acorn": "^7.1.1",
+ "acorn-globals": "^6.0.0",
+ "cssom": "^0.4.4",
+ "cssstyle": "^2.2.0",
+ "data-urls": "^2.0.0",
+ "decimal.js": "^10.2.0",
+ "domexception": "^2.0.1",
+ "escodegen": "^1.14.1",
+ "html-encoding-sniffer": "^2.0.1",
+ "is-potential-custom-element-name": "^1.0.0",
+ "nwsapi": "^2.2.0",
+ "parse5": "5.1.1",
+ "request": "^2.88.2",
+ "request-promise-native": "^1.0.8",
+ "saxes": "^5.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^3.0.1",
+ "w3c-hr-time": "^1.0.2",
+ "w3c-xmlserializer": "^2.0.0",
+ "webidl-conversions": "^6.1.0",
+ "whatwg-encoding": "^1.0.5",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.0.0",
+ "ws": "^7.2.3",
+ "xml-name-validator": "^3.0.0"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true
+ },
+ "tough-cookie": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
+ "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
+ "dev": true,
+ "requires": {
+ "ip-regex": "^2.1.0",
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ },
+ "webidl-conversions": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
+ "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
+ "dev": true
+ },
+ "ws": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
+ "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
+ "dev": true
+ }
+ }
+ },
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
},
+ "json-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
+ "dev": true
+ },
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -8376,8 +29360,7 @@
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-stringify-safe": {
"version": "5.0.1",
@@ -8392,10 +29375,13 @@
"dev": true
},
"json5": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
- "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
- "dev": true
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
+ "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
},
"jsonfile": {
"version": "4.0.0",
@@ -8412,6 +29398,16 @@
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"dev": true
},
+ "JSONStream": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
+ "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+ "dev": true,
+ "requires": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ }
+ },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -8424,10 +29420,15 @@
"verror": "1.10.0"
}
},
+ "jstz": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/jstz/-/jstz-2.1.1.tgz",
+ "integrity": "sha512-8hfl5RD6P7rEeIbzStBz3h4f+BQHfq/ABtoU6gXKQv5OcZhnmrIpG7e1pYaZ8hS9e0mp+bxUj08fnDUbKctYyA=="
+ },
"jszip": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.0.tgz",
- "integrity": "sha512-4WjbsaEtBK/DHeDZOPiPw5nzSGLDEDDreFRDEgnoMwmknPjTqa+23XuYFk6NiGbeiAeZCctiQ/X/z0lQBmDVOQ==",
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
+ "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
"dev": true,
"requires": {
"lie": "~3.3.0",
@@ -8437,70 +29438,129 @@
}
},
"karma": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/karma/-/karma-4.0.1.tgz",
- "integrity": "sha512-ind+4s03BqIXas7ZmraV3/kc5+mnqwCd+VDX1FndS6jxbt03kQKX2vXrWxNLuCjVYmhMwOZosAEKMM0a2q7w7A==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/karma/-/karma-5.0.9.tgz",
+ "integrity": "sha512-dUA5z7Lo7G4FRSe1ZAXqOINEEWxmCjDBbfRBmU/wYlSMwxUQJP/tEEP90yJt3Uqo03s9rCgVnxtlfq+uDhxSPg==",
"dev": true,
"requires": {
- "bluebird": "^3.3.0",
- "body-parser": "^1.16.1",
- "braces": "^2.3.2",
- "chokidar": "^2.0.3",
- "colors": "^1.1.0",
- "connect": "^3.6.0",
- "core-js": "^2.2.0",
+ "body-parser": "^1.19.0",
+ "braces": "^3.0.2",
+ "chokidar": "^3.0.0",
+ "colors": "^1.4.0",
+ "connect": "^3.7.0",
"di": "^0.0.1",
- "dom-serialize": "^2.2.0",
- "flatted": "^2.0.0",
- "glob": "^7.1.1",
- "graceful-fs": "^4.1.2",
- "http-proxy": "^1.13.0",
- "isbinaryfile": "^3.0.0",
- "lodash": "^4.17.11",
- "log4js": "^4.0.0",
- "mime": "^2.3.1",
- "minimatch": "^3.0.2",
- "optimist": "^0.6.1",
- "qjobs": "^1.1.4",
- "range-parser": "^1.2.0",
- "rimraf": "^2.6.0",
- "safe-buffer": "^5.0.1",
- "socket.io": "2.1.1",
+ "dom-serialize": "^2.2.1",
+ "flatted": "^2.0.2",
+ "glob": "^7.1.6",
+ "graceful-fs": "^4.2.4",
+ "http-proxy": "^1.18.1",
+ "isbinaryfile": "^4.0.6",
+ "lodash": "^4.17.15",
+ "log4js": "^6.2.1",
+ "mime": "^2.4.5",
+ "minimatch": "^3.0.4",
+ "qjobs": "^1.2.0",
+ "range-parser": "^1.2.1",
+ "rimraf": "^3.0.2",
+ "socket.io": "^2.3.0",
"source-map": "^0.6.1",
- "tmp": "0.0.33",
- "useragent": "2.3.0"
+ "tmp": "0.2.1",
+ "ua-parser-js": "0.7.21",
+ "yargs": "^15.3.1"
},
"dependencies": {
- "chokidar": {
- "version": "2.1.8",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
- "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
- "anymatch": "^2.0.0",
- "async-each": "^1.0.1",
- "braces": "^2.3.2",
- "fsevents": "^1.2.7",
- "glob-parent": "^3.1.0",
- "inherits": "^2.0.3",
- "is-binary-path": "^1.0.0",
- "is-glob": "^4.0.0",
- "normalize-path": "^3.0.0",
- "path-is-absolute": "^1.0.0",
- "readdirp": "^2.2.1",
- "upath": "^1.1.1"
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
}
},
"mime": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
- "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
+ "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==",
"dev": true
},
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"source-map": {
@@ -8509,21 +29569,83 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
- "upath": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
- "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
- "dev": true
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "tmp": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+ "dev": true,
+ "requires": {
+ "rimraf": "^3.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
}
}
},
"karma-chrome-launcher": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
- "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz",
+ "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==",
"dev": true,
"requires": {
- "fs-access": "^1.0.0",
"which": "^1.2.1"
}
},
@@ -8534,42 +29656,34 @@
"dev": true,
"requires": {
"resolve": "^1.3.3"
- },
- "dependencies": {
- "resolve": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
- "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
- "dev": true,
- "requires": {
- "path-parse": "^1.0.6"
- }
- }
}
},
"karma-coverage-istanbul-reporter": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.5.tgz",
- "integrity": "sha512-yPvAlKtY3y+rKKWbOo0CzBMVTvJEeMOgbMXuVv3yWvS8YtYKC98AU9vFF0mVBZ2RP1E9SgS90+PT6Kf14P3S4w==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz",
+ "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==",
"dev": true,
"requires": {
- "istanbul-api": "^2.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^3.0.6",
+ "istanbul-reports": "^3.0.2",
"minimatch": "^3.0.4"
}
},
"karma-jasmine": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz",
- "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.3.1.tgz",
+ "integrity": "sha512-Nxh7eX9mOQMyK0VSsMxdod+bcqrR/ikrmEiWj5M6fwuQ7oI+YEF1FckaDsWfs6TIpULm9f0fTKMjF7XcrvWyqQ==",
"dev": true,
"requires": {
- "jasmine-core": "^3.3"
+ "jasmine-core": "^3.5.0"
}
},
"karma-jasmine-html-reporter": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.4.0.tgz",
- "integrity": "sha512-0wxhwA8PLPpICZ4o2GRnPi67hf3JhfQm5WCB8nElh4qsE6wRNOTtrqooyBPNqI087Xr2SBhxLg5fU+BJ/qxRrw==",
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.5.4.tgz",
+ "integrity": "sha512-PtilRLno5O6wH3lDihRnz0Ba8oSn0YUJqKjjux1peoYGwo0AQqrWRbdWk/RLzcGlb+onTyXAnHl6M+Hu3UxG/Q==",
"dev": true
},
"karma-source-map-support": {
@@ -8581,6 +29695,15 @@
"source-map-support": "^0.5.5"
}
},
+ "keyv": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+ "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+ "dev": true,
+ "requires": {
+ "json-buffer": "3.0.0"
+ }
+ },
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -8588,35 +29711,43 @@
"dev": true
},
"kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true
},
+ "latest-version": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+ "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
+ "dev": true,
+ "requires": {
+ "package-json": "^6.3.0"
+ }
+ },
"lcid": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
- "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+ "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
"dev": true,
"requires": {
- "invert-kv": "^2.0.0"
+ "invert-kv": "^1.0.0"
}
},
"less": {
- "version": "3.10.3",
- "resolved": "https://registry.npmjs.org/less/-/less-3.10.3.tgz",
- "integrity": "sha512-vz32vqfgmoxF1h3K4J+yKCtajH0PWmjkIFgbs5d78E/c/e+UQTnI+lWK+1eQRE95PXM2mC3rJlLSSP9VQHnaow==",
+ "version": "3.12.2",
+ "resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
+ "integrity": "sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q==",
"dev": true,
"requires": {
- "clone": "^2.1.2",
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
+ "make-dir": "^2.1.0",
"mime": "^1.4.1",
- "mkdirp": "^0.5.0",
- "promise": "^7.1.1",
- "request": "^2.83.0",
- "source-map": "~0.6.0"
+ "native-request": "^1.0.5",
+ "source-map": "~0.6.0",
+ "tslib": "^1.10.0"
},
"dependencies": {
"source-map": {
@@ -8625,24 +29756,202 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
}
}
},
"less-loader": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-5.0.0.tgz",
- "integrity": "sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-6.1.0.tgz",
+ "integrity": "sha512-/jLzOwLyqJ7Kt3xg5sHHkXtOyShWwFj410K9Si9WO+/h8rmYxxkSR0A3/hFEntWudE20zZnWMtpMYnLzqTVdUA==",
"dev": true,
"requires": {
- "clone": "^2.1.1",
- "loader-utils": "^1.1.0",
- "pify": "^4.0.1"
+ "clone": "^2.1.2",
+ "less": "^3.11.1",
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.6"
+ }
+ },
+ "level": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz",
+ "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==",
+ "optional": true,
+ "requires": {
+ "level-js": "^5.0.0",
+ "level-packager": "^5.1.0",
+ "leveldown": "^5.4.0"
+ }
+ },
+ "level-codec": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz",
+ "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==",
+ "optional": true,
+ "requires": {
+ "buffer": "^5.6.0"
+ },
+ "dependencies": {
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "optional": true,
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ }
+ }
+ },
+ "level-concat-iterator": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz",
+ "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==",
+ "optional": true
+ },
+ "level-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz",
+ "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==",
+ "optional": true,
+ "requires": {
+ "errno": "~0.1.1"
+ }
+ },
+ "level-iterator-stream": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz",
+ "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==",
+ "optional": true,
+ "requires": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0",
+ "xtend": "^4.0.2"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "optional": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ }
+ }
+ },
+ "level-js": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz",
+ "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==",
+ "optional": true,
+ "requires": {
+ "abstract-leveldown": "~6.2.3",
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.3",
+ "ltgt": "^2.1.2"
+ },
+ "dependencies": {
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "optional": true,
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ }
+ }
+ },
+ "level-packager": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz",
+ "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==",
+ "optional": true,
+ "requires": {
+ "encoding-down": "^6.3.0",
+ "levelup": "^4.3.2"
+ }
+ },
+ "level-supports": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz",
+ "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==",
+ "optional": true,
+ "requires": {
+ "xtend": "^4.0.2"
+ }
+ },
+ "leveldown": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz",
+ "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==",
+ "optional": true,
+ "requires": {
+ "abstract-leveldown": "~6.2.1",
+ "napi-macros": "~2.0.0",
+ "node-gyp-build": "~4.1.0"
+ }
+ },
+ "levelup": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz",
+ "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==",
+ "optional": true,
+ "requires": {
+ "deferred-leveldown": "~5.3.0",
+ "level-errors": "~2.0.0",
+ "level-iterator-stream": "~4.0.0",
+ "level-supports": "~1.0.0",
+ "xtend": "~4.0.0"
+ }
+ },
+ "leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true
+ },
+ "levenary": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz",
+ "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==",
+ "dev": true,
+ "requires": {
+ "leven": "^3.1.0"
+ }
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "lib0": {
+ "version": "0.2.35",
+ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.35.tgz",
+ "integrity": "sha512-drVD3EscB3TIxiFzceuZg7oF5Z6I8a0KX+7FowNcAXOEsTej/hlHB+ElJ8Pa/Ge73Gy3fklSJtPxpNd2PajdWg==",
+ "requires": {
+ "isomorphic.js": "^0.1.3"
}
},
"license-webpack-plugin": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.1.3.tgz",
- "integrity": "sha512-vTSY5r9HOq4sxR2BIxdIXWKI+9n3b+DoQkhKHedB3TdSxTfXUDRxKXdAj5iejR+qNXprXsxvEu9W+zOhgGIkAw==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.2.0.tgz",
+ "integrity": "sha512-XPsdL/0brSHf+7dXIlRqotnCQ58RX2au6otkOg4U3dm8uH+Ka/fW4iukEs95uXm+qKe/SBs+s1Ll/aQddKG+tg==",
"dev": true,
"requires": {
"@types/webpack-sources": "^0.1.5",
@@ -8658,6 +29967,41 @@
"immediate": "~3.0.5"
}
},
+ "limiter": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==",
+ "dev": true
+ },
+ "load-json-file": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+ "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "dependencies": {
+ "parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "dev": true,
+ "requires": {
+ "error-ex": "^1.2.0"
+ }
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
"loader-runner": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@@ -8665,36 +30009,45 @@
"dev": true
},
"loader-utils": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
- "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
+ "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ }
+ },
+ "localtunnel": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.0.tgz",
+ "integrity": "sha512-g6E0aLgYYDvQDxIjIXkgJo2+pHj3sGg4Wz/XP3h2KtZnRsWPbOQY+hw1H8Z91jep998fkcVE9l+kghO+97vllg==",
"dev": true,
"requires": {
- "big.js": "^5.2.2",
- "emojis-list": "^2.0.0",
- "json5": "^1.0.1"
+ "axios": "0.19.0",
+ "debug": "4.1.1",
+ "openurl": "1.1.1",
+ "yargs": "13.3.0"
},
"dependencies": {
- "big.js": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
- "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
- "dev": true
- },
- "json5": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
- "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "yargs": {
+ "version": "13.3.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
+ "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==",
"dev": true,
"requires": {
- "minimist": "^1.2.0"
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.1"
}
- },
- "minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
- "dev": true
}
}
},
@@ -8709,9 +30062,9 @@
}
},
"lodash": {
- "version": "4.17.14",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
- "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==",
+ "version": "4.17.20",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
},
"lodash.clonedeep": {
@@ -8720,12 +30073,29 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
+ "lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
+ },
+ "lodash.isfinite": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz",
+ "integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=",
+ "dev": true
+ },
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
+ "lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
+ "dev": true
+ },
"lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -8742,39 +30112,22 @@
}
},
"log4js": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.0.2.tgz",
- "integrity": "sha512-KE7HjiieVDPPdveA3bJZSuu0n8chMkFl8mIoisBFxwEJ9FmXe4YzNuiqSwYUiR1K8q8/5/8Yd6AClENY1RA9ww==",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz",
+ "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==",
"dev": true,
"requires": {
- "date-format": "^2.0.0",
- "debug": "^3.1.0",
- "flatted": "^2.0.0",
- "rfdc": "^1.1.2",
- "streamroller": "^1.0.1"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
- }
+ "date-format": "^3.0.0",
+ "debug": "^4.1.1",
+ "flatted": "^2.0.1",
+ "rfdc": "^1.1.4",
+ "streamroller": "^2.2.4"
}
},
"loglevel": {
- "version": "1.6.7",
- "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz",
- "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==",
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz",
+ "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==",
"dev": true
},
"loose-envify": {
@@ -8786,20 +30139,39 @@
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
+ "lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+ "dev": true
+ },
"lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
- "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
- "pseudomap": "^1.0.2",
- "yallist": "^2.1.2"
+ "yallist": "^3.0.2"
+ },
+ "dependencies": {
+ "yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ }
}
},
+ "ltgt": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz",
+ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=",
+ "optional": true
+ },
"magic-string": {
- "version": "0.25.4",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.4.tgz",
- "integrity": "sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw==",
+ "version": "0.25.7",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+ "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
@@ -8824,9 +30196,9 @@
}
},
"make-error": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
- "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"make-fetch-happen": {
@@ -8848,16 +30220,10 @@
"ssri": "^6.0.0"
},
"dependencies": {
- "bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
- "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
- },
"cacache": {
- "version": "12.0.3",
- "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz",
- "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz",
+ "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
@@ -8877,27 +30243,19 @@
"y18n": "^4.0.0"
}
},
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
+ "chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true
},
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
- "yallist": "^3.0.2"
+ "glob": "^7.1.3"
}
},
"ssri": {
@@ -8908,30 +30266,9 @@
"requires": {
"figgy-pudding": "^3.5.1"
}
- },
- "yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
}
}
},
- "mamacro": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz",
- "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==",
- "dev": true
- },
- "map-age-cleaner": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
- "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
- "dev": true,
- "requires": {
- "p-defer": "^1.0.0"
- }
- },
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -8967,18 +30304,23 @@
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
- "dev": true
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"mem": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
- "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
+ "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
"dev": true,
"requires": {
- "map-age-cleaner": "^0.1.1",
- "mimic-fn": "^2.0.0",
- "p-is-promise": "^2.0.0"
+ "mimic-fn": "^1.0.0"
+ },
+ "dependencies": {
+ "mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+ "dev": true
+ }
}
},
"memory-fs": {
@@ -8994,8 +30336,7 @@
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
- "dev": true
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"merge-source-map": {
"version": "1.1.0",
@@ -9020,31 +30361,25 @@
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true
+ },
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
- "dev": true
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"micromatch": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
- "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"dev": true,
"requires": {
- "arr-diff": "^4.0.0",
- "array-unique": "^0.3.2",
- "braces": "^2.3.1",
- "define-property": "^2.0.2",
- "extend-shallow": "^3.0.2",
- "extglob": "^2.0.4",
- "fragment-cache": "^0.2.1",
- "kind-of": "^6.0.2",
- "nanomatch": "^1.2.9",
- "object.pick": "^1.3.0",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.2"
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
}
},
"miller-rabin": {
@@ -9055,27 +30390,32 @@
"requires": {
"bn.js": "^4.0.0",
"brorand": "^1.0.1"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ }
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "dev": true
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
- "version": "1.38.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
- "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==",
- "dev": true
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
+ "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
},
"mime-types": {
- "version": "2.1.22",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
- "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
- "dev": true,
+ "version": "2.1.27",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
+ "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"requires": {
- "mime-db": "~1.38.0"
+ "mime-db": "1.44.0"
}
},
"mimic-fn": {
@@ -9084,10 +30424,16 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
+ "mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true
+ },
"mini-css-extract-plugin": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz",
- "integrity": "sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw==",
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz",
+ "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0",
@@ -9096,6 +30442,26 @@
"webpack-sources": "^1.1.0"
},
"dependencies": {
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ },
"normalize-url": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
@@ -9107,6 +30473,17 @@
"query-string": "^4.1.0",
"sort-keys": "^1.0.0"
}
+ },
+ "schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ }
}
}
},
@@ -9132,27 +30509,18 @@
}
},
"minimist": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
- "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"minipass": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
- "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
+ "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
"dev": true,
"requires": {
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.0"
- },
- "dependencies": {
- "yallist": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
- "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
- "dev": true
- }
+ "yallist": "^4.0.0"
}
},
"minipass-collect": {
@@ -9162,23 +30530,6 @@
"dev": true,
"requires": {
"minipass": "^3.0.0"
- },
- "dependencies": {
- "minipass": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz",
- "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- }
}
},
"minipass-flush": {
@@ -9188,58 +30539,25 @@
"dev": true,
"requires": {
"minipass": "^3.0.0"
- },
- "dependencies": {
- "minipass": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz",
- "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- }
}
},
"minipass-pipeline": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz",
- "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
"dev": true,
"requires": {
"minipass": "^3.0.0"
- },
- "dependencies": {
- "minipass": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz",
- "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- }
}
},
"minizlib": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
- "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dev": true,
"requires": {
- "minipass": "^2.2.1"
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
}
},
"mississippi": {
@@ -9260,6 +30578,12 @@
"through2": "^2.0.0"
}
},
+ "mitt": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz",
+ "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==",
+ "dev": true
+ },
"mixin-deep": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
@@ -9282,12 +30606,12 @@
}
},
"mkdirp": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
- "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
- "minimist": "0.0.8"
+ "minimist": "^1.2.5"
}
},
"move-concurrently": {
@@ -9302,12 +30626,40 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.3"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
}
},
- "ms": {
+ "move-file": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "resolved": "https://registry.npmjs.org/move-file/-/move-file-2.0.0.tgz",
+ "integrity": "sha512-cdkdhNCgbP5dvS4tlGxZbD+nloio9GIimP57EjqFhwLcMjnU+XJKAZzlmg/TN/AK1LuNAdTSvm3CPPP4Xkv0iQ==",
+ "dev": true,
+ "requires": {
+ "path-exists": "^4.0.0"
+ },
+ "dependencies": {
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"multicast-dns": {
@@ -9330,14 +30682,7 @@
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
- "dev": true
- },
- "nan": {
- "version": "2.12.1",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
- "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==",
- "dev": true,
- "optional": true
+ "dev": true
},
"nanomatch": {
"version": "1.2.13",
@@ -9358,22 +30703,48 @@
"to-regex": "^3.0.1"
}
},
+ "napi-macros": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz",
+ "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==",
+ "optional": true
+ },
+ "native-request": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.7.tgz",
+ "integrity": "sha512-9nRjinI9bmz+S7dgNtf4A70+/vPhnd+2krGpy4SUlADuOuSa24IDkNaZ+R/QT1wQ6S8jBdi6wE7fLekFZNfUpQ==",
+ "dev": true,
+ "optional": true
+ },
"negotiator": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
- "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
- "dev": true
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"neo-async": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
- "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==",
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true
+ },
+ "next-tick": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
"dev": true
},
"ngx-bootstrap": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-5.5.0.tgz",
- "integrity": "sha512-BJeghbkKFQl49sg3GIYQyjvwaHn64xFOsinBVD8HWKOVpRJSnuafrjXByGDtfq35jGY4R+7iBLksM1IYLUPshg=="
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-5.6.1.tgz",
+ "integrity": "sha512-8fDs3VaaWgKpupakPKS0QaUc+1E/JMBGJDxUUODjyIkLtFr1A8vH4cjXiV3AfrPvhK27GH0oyTPyKWKcCjEtVg=="
+ },
+ "ngx-toastr": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-13.2.0.tgz",
+ "integrity": "sha512-XU+wACX5hxwOJ4BtPMAUExQmYbjfvH3C/R4vcC9QK/dX2Zw+2w9tS9m4W6TUFyR92xZ/tGLBtsqRdrDRn3fJCw==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
},
"nice-try": {
"version": "1.0.5",
@@ -9382,9 +30753,9 @@
"dev": true
},
"node-fetch-npm": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz",
- "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz",
+ "integrity": "sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==",
"dev": true,
"requires": {
"encoding": "^0.1.11",
@@ -9393,9 +30764,9 @@
}
},
"node-forge": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
- "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
+ "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"dev": true
},
"node-gyp": {
@@ -9417,35 +30788,29 @@
"which": "1"
},
"dependencies": {
- "semver": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
- "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
- "dev": true
- },
- "tar": {
- "version": "4.4.10",
- "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz",
- "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==",
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
- "chownr": "^1.1.1",
- "fs-minipass": "^1.2.5",
- "minipass": "^2.3.5",
- "minizlib": "^1.2.1",
- "mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.3"
+ "glob": "^7.1.3"
}
},
- "yallist": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
- "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
+ "semver": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true
}
}
},
+ "node-gyp-build": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz",
+ "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==",
+ "optional": true
+ },
"node-libs-browser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@@ -9477,27 +30842,66 @@
"vm-browserify": "^1.0.1"
},
"dependencies": {
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
+ },
+ "util": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
+ "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
+ "dev": true,
+ "requires": {
+ "inherits": "2.0.3"
+ }
}
}
},
"node-releases": {
- "version": "1.1.49",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.49.tgz",
- "integrity": "sha512-xH8t0LS0disN0mtRCh+eByxFPie+msJUBL/lJDBuap53QGiYPa9joh83K4pCZgWJ+2L4b9h88vCVdXQ60NO2bg==",
+ "version": "1.1.60",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz",
+ "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==",
+ "dev": true
+ },
+ "nodemon": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz",
+ "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==",
"dev": true,
"requires": {
- "semver": "^6.3.0"
+ "chokidar": "^3.2.2",
+ "debug": "^3.2.6",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.0.4",
+ "pstree.remy": "^1.1.7",
+ "semver": "^5.7.1",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.2",
+ "update-notifier": "^4.0.0"
},
"dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
"semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
@@ -9524,21 +30928,24 @@
},
"dependencies": {
"hosted-git-info": {
- "version": "2.8.5",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
- "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==",
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
"normalize-path": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
- "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
- "dev": true,
- "requires": {
- "remove-trailing-separator": "^1.0.1"
- }
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
},
"normalize-range": {
"version": "0.1.2",
@@ -9561,6 +30968,15 @@
"npm-normalize-package-bin": "^1.0.1"
}
},
+ "npm-install-checks": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz",
+ "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==",
+ "dev": true,
+ "requires": {
+ "semver": "^7.1.1"
+ }
+ },
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
@@ -9568,29 +30984,14 @@
"dev": true
},
"npm-package-arg": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz",
- "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz",
+ "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==",
"dev": true,
"requires": {
- "hosted-git-info": "^2.7.1",
- "osenv": "^0.1.5",
- "semver": "^5.6.0",
+ "hosted-git-info": "^3.0.2",
+ "semver": "^7.0.0",
"validate-npm-package-name": "^3.0.0"
- },
- "dependencies": {
- "hosted-git-info": {
- "version": "2.8.5",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
- "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==",
- "dev": true
- },
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
}
},
"npm-packlist": {
@@ -9605,50 +31006,59 @@
}
},
"npm-pick-manifest": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz",
- "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz",
+ "integrity": "sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw==",
"dev": true,
"requires": {
- "figgy-pudding": "^3.5.1",
- "npm-package-arg": "^6.0.0",
- "semver": "^5.4.1"
+ "npm-install-checks": "^4.0.0",
+ "npm-package-arg": "^8.0.0",
+ "semver": "^7.0.0"
}
},
"npm-registry-fetch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.2.tgz",
- "integrity": "sha512-Z0IFtPEozNdeZRPh3aHHxdG+ZRpzcbQaJLthsm3VhNf6DScicTFRHZzK82u8RsJUsUHkX+QH/zcB/5pmd20H4A==",
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz",
+ "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==",
"dev": true,
"requires": {
- "JSONStream": "^1.3.4",
"bluebird": "^3.5.1",
"figgy-pudding": "^3.4.1",
+ "JSONStream": "^1.3.4",
"lru-cache": "^5.1.1",
"make-fetch-happen": "^5.0.0",
"npm-package-arg": "^6.1.0",
"safe-buffer": "^5.2.0"
},
"dependencies": {
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "hosted-git-info": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+ "dev": true
+ },
+ "npm-package-arg": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz",
+ "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==",
"dev": true,
"requires": {
- "yallist": "^3.0.2"
+ "hosted-git-info": "^2.7.1",
+ "osenv": "^0.1.5",
+ "semver": "^5.6.0",
+ "validate-npm-package-name": "^3.0.0"
}
},
"safe-buffer": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
- "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
- "yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
@@ -9688,12 +31098,6 @@
"boolbase": "~1.0.0"
}
},
- "null-check": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz",
- "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=",
- "dev": true
- },
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
@@ -9706,6 +31110,12 @@
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
+ "nwsapi": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
+ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
+ "dev": true
+ },
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -9756,16 +31166,20 @@
}
},
"object-inspect": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
- "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
+ "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
"dev": true
},
"object-is": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz",
- "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==",
- "dev": true
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz",
+ "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
},
"object-keys": {
"version": "1.1.1",
@@ -9773,6 +31187,12 @@
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
},
+ "object-path": {
+ "version": "0.11.5",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.5.tgz",
+ "integrity": "sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg==",
+ "dev": true
+ },
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
@@ -9835,7 +31255,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
- "dev": true,
"requires": {
"ee-first": "1.1.1"
}
@@ -9850,29 +31269,35 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
"requires": {
"wrappy": "1"
}
},
"onetime": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
- "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"requires": {
"mimic-fn": "^2.1.0"
}
},
"open": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/open/-/open-7.0.0.tgz",
- "integrity": "sha512-K6EKzYqnwQzk+/dzJAQSBORub3xlBTxMz+ntpZpH/LyCa1o6KjXhuN+2npAaI9jaSmU3R1Q8NWf4KUWcyytGsQ==",
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz",
+ "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==",
"dev": true,
"requires": {
- "is-wsl": "^2.1.0"
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
}
},
+ "openurl": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz",
+ "integrity": "sha1-OHW0sO96UsFW8NtB1GCduw+Us4c=",
+ "dev": true
+ },
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@@ -9890,52 +31315,99 @@
}
}
},
- "optimist": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
- "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"dev": true,
"requires": {
- "minimist": "~0.0.1",
- "wordwrap": "~0.0.2"
- },
- "dependencies": {
- "wordwrap": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
- "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
- "dev": true
- }
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
}
},
"ora": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.2.tgz",
- "integrity": "sha512-YUOZbamht5mfLxPmk4M35CD/5DuOkAacxlEUbStVXpBAt4fyhBf+vZHI/HRkI++QUp3sNoeA2Gw4C+hi4eGSig==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.4.tgz",
+ "integrity": "sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww==",
"dev": true,
"requires": {
- "chalk": "^2.4.2",
+ "chalk": "^3.0.0",
"cli-cursor": "^3.1.0",
"cli-spinners": "^2.2.0",
"is-interactive": "^1.0.0",
"log-symbols": "^3.0.0",
- "strip-ansi": "^5.2.0",
+ "mute-stream": "0.0.8",
+ "strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"dependencies": {
"ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
- "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
- "ansi-regex": "^4.1.0"
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
}
}
}
@@ -9962,14 +31434,64 @@
"dev": true
},
"os-locale": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
- "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
+ "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
"dev": true,
"requires": {
- "execa": "^1.0.0",
- "lcid": "^2.0.0",
- "mem": "^4.0.0"
+ "execa": "^0.7.0",
+ "lcid": "^1.0.0",
+ "mem": "^1.1.0"
+ },
+ "dependencies": {
+ "cross-spawn": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "execa": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+ "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^5.0.1",
+ "get-stream": "^3.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "get-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "dev": true,
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+ "dev": true
+ }
}
},
"os-tmpdir": {
@@ -9988,28 +31510,27 @@
"os-tmpdir": "^1.0.0"
}
},
- "p-defer": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
- "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
+ "outdent": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.7.1.tgz",
+ "integrity": "sha512-VjIzdUHunL74DdhcwMDt5FhNDQ8NYmTkuW0B+usIV2afS9aWT/1c9z1TsnFW349TP3nxmYeUl7Z++XpJRByvgg=="
+ },
+ "p-cancelable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
"dev": true
},
"p-finally": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
- "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
- "dev": true
- },
- "p-is-promise": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
- "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true
},
"p-limit": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
- "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
@@ -10025,9 +31546,9 @@
}
},
"p-map": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
- "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dev": true,
"requires": {
"aggregate-error": "^3.0.0"
@@ -10048,10 +31569,30 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
+ "package-json": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
+ "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
+ "dev": true,
+ "requires": {
+ "got": "^9.6.0",
+ "registry-auth-token": "^4.0.0",
+ "registry-url": "^5.0.0",
+ "semver": "^6.2.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
"pacote": {
- "version": "9.5.8",
- "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.5.8.tgz",
- "integrity": "sha512-0Tl8Oi/K0Lo4MZmH0/6IsT3gpGf9eEAznLXEQPKgPq7FscnbUOyopnVpwXlnQdIbCUaojWy1Wd7VMyqfVsRrIw==",
+ "version": "9.5.12",
+ "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.5.12.tgz",
+ "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==",
"dev": true,
"requires": {
"bluebird": "^3.5.3",
@@ -10068,6 +31609,7 @@
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"normalize-package-data": "^2.4.0",
+ "npm-normalize-package-bin": "^1.0.0",
"npm-package-arg": "^6.1.0",
"npm-packlist": "^1.1.12",
"npm-pick-manifest": "^3.0.0",
@@ -10086,9 +31628,9 @@
},
"dependencies": {
"cacache": {
- "version": "12.0.3",
- "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz",
- "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz",
+ "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
@@ -10106,28 +31648,6 @@
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
- },
- "dependencies": {
- "bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
- "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
- },
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- }
}
},
"chownr": {
@@ -10136,13 +31656,52 @@
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "hosted-git-info": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+ "dev": true
+ },
+ "minipass": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
+ "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "npm-package-arg": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz",
+ "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.7.1",
+ "osenv": "^0.1.5",
+ "semver": "^5.6.0",
+ "validate-npm-package-name": "^3.0.0"
+ }
+ },
+ "npm-pick-manifest": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz",
+ "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==",
+ "dev": true,
+ "requires": {
+ "figgy-pudding": "^3.5.1",
+ "npm-package-arg": "^6.0.0",
+ "semver": "^5.4.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
- "yallist": "^3.0.2"
+ "glob": "^7.1.3"
}
},
"semver": {
@@ -10160,33 +31719,6 @@
"figgy-pudding": "^3.5.1"
}
},
- "tar": {
- "version": "4.4.13",
- "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
- "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
- "dev": true,
- "requires": {
- "chownr": "^1.1.1",
- "fs-minipass": "^1.2.5",
- "minipass": "^2.8.6",
- "minizlib": "^1.2.1",
- "mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.3"
- },
- "dependencies": {
- "minipass": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
- "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
- "dev": true,
- "requires": {
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.0"
- }
- }
- }
- },
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -10196,9 +31728,9 @@
}
},
"pako": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
- "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==",
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
},
"parallel-transform": {
@@ -10213,14 +31745,13 @@
}
},
"parse-asn1": {
- "version": "5.1.5",
- "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz",
- "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==",
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
+ "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==",
"dev": true,
"requires": {
- "asn1.js": "^4.0.0",
+ "asn1.js": "^5.2.0",
"browserify-aes": "^1.0.0",
- "create-hash": "^1.1.0",
"evp_bytestokey": "^1.0.0",
"pbkdf2": "^3.0.3",
"safe-buffer": "^5.1.1"
@@ -10239,8 +31770,7 @@
"parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
- "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
- "optional": true
+ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
},
"parseqs": {
"version": "0.0.5",
@@ -10261,10 +31791,9 @@
}
},
"parseurl": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
- "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
- "dev": true
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"pascalcase": {
"version": "0.1.1",
@@ -10317,30 +31846,18 @@
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
- "dev": true
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"path-type": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
- "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
- "dev": true,
- "requires": {
- "pify": "^3.0.0"
- },
- "dependencies": {
- "pify": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
- "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
- }
- }
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true
},
"pbkdf2": {
- "version": "3.0.17",
- "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
- "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
+ "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
"dev": true,
"requires": {
"create-hash": "^1.1.2",
@@ -10357,9 +31874,9 @@
"dev": true
},
"picomatch": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
- "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"pify": {
@@ -10392,15 +31909,24 @@
"find-up": "^3.0.0"
}
},
+ "pnp-webpack-plugin": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
+ "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==",
+ "dev": true,
+ "requires": {
+ "ts-pnp": "^1.1.6"
+ }
+ },
"portfinder": {
- "version": "1.0.25",
- "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
- "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==",
+ "version": "1.0.28",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
+ "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
"dev": true,
"requires": {
"async": "^2.6.2",
"debug": "^3.1.1",
- "mkdirp": "^0.5.1"
+ "mkdirp": "^0.5.5"
},
"dependencies": {
"debug": {
@@ -10411,11 +31937,23 @@
"requires": {
"ms": "^2.1.1"
}
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ }
+ }
+ },
+ "portscanner": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.1.1.tgz",
+ "integrity": "sha1-6rtAnk3iSVD1oqUW01rnaTQ/u5Y=",
+ "dev": true,
+ "requires": {
+ "async": "1.5.2",
+ "is-number-like": "^1.0.3"
+ },
+ "dependencies": {
+ "async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"dev": true
}
}
@@ -10427,9 +31965,9 @@
"dev": true
},
"postcss": {
- "version": "7.0.21",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz",
- "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==",
+ "version": "7.0.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.31.tgz",
+ "integrity": "sha512-a937VDHE1ftkjk+8/7nj/mrjtmkn69xxzJgRETXdAUU+IgOYPQNJF17haGWbeDxSyk++HA14UA98FurvPyBJOA==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@@ -10442,27 +31980,27 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
+ },
+ "supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
}
}
},
"postcss-calc": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz",
- "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==",
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.3.tgz",
+ "integrity": "sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA==",
"dev": true,
"requires": {
- "css-unit-converter": "^1.1.1",
- "postcss": "^7.0.5",
- "postcss-selector-parser": "^5.0.0-rc.4",
- "postcss-value-parser": "^3.3.1"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
- "dev": true
- }
+ "postcss": "^7.0.27",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.0.2"
}
},
"postcss-colormin": {
@@ -10580,6 +32118,39 @@
"postcss": "^7.0.0",
"postcss-load-config": "^2.0.0",
"schema-utils": "^1.0.0"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ },
+ "schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ }
+ }
}
},
"postcss-merge-longhand": {
@@ -10617,12 +32188,12 @@
},
"dependencies": {
"postcss-selector-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
- "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
+ "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
"dev": true,
"requires": {
- "dot-prop": "^4.1.1",
+ "dot-prop": "^5.2.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1"
}
@@ -10702,18 +32273,87 @@
},
"dependencies": {
"postcss-selector-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
- "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
+ "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
"dev": true,
"requires": {
- "dot-prop": "^4.1.1",
+ "dot-prop": "^5.2.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1"
}
}
}
},
+ "postcss-modules-extract-imports": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz",
+ "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.5"
+ }
+ },
+ "postcss-modules-local-by-default": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz",
+ "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==",
+ "dev": true,
+ "requires": {
+ "icss-utils": "^4.1.1",
+ "postcss": "^7.0.32",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "dependencies": {
+ "postcss": {
+ "version": "7.0.32",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
+ "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.2",
+ "source-map": "^0.6.1",
+ "supports-color": "^6.1.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "postcss-modules-scope": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz",
+ "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.6",
+ "postcss-selector-parser": "^6.0.0"
+ }
+ },
+ "postcss-modules-values": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz",
+ "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==",
+ "dev": true,
+ "requires": {
+ "icss-utils": "^4.0.0",
+ "postcss": "^7.0.6"
+ }
+ },
"postcss-normalize-charset": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz",
@@ -10929,22 +32569,14 @@
}
},
"postcss-selector-parser": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
- "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
+ "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
"dev": true,
"requires": {
- "cssesc": "^2.0.0",
+ "cssesc": "^3.0.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1"
- },
- "dependencies": {
- "cssesc": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
- "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
- "dev": true
- }
}
},
"postcss-svgo": {
@@ -10979,9 +32611,15 @@
}
},
"postcss-value-parser": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz",
- "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
+ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
"dev": true
},
"prepend-http": {
@@ -10990,12 +32628,6 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
},
- "private": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
- "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
- "dev": true
- },
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -11003,21 +32635,11 @@
"dev": true
},
"process-nextick-args": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
- "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
- "promise": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
- "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
- "dev": true,
- "optional": true,
- "requires": {
- "asap": "~2.0.3"
- }
- },
"promise-inflight": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -11052,9 +32674,9 @@
}
},
"protractor": {
- "version": "5.4.2",
- "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.4.2.tgz",
- "integrity": "sha512-zlIj64Cr6IOWP7RwxVeD8O4UskLYPoyIcg0HboWJL9T79F1F0VWtKkGTr/9GN6BKL+/Q/GmM7C9kFVCfDbP5sA==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz",
+ "integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==",
"dev": true,
"requires": {
"@types/q": "^0.0.32",
@@ -11065,34 +32687,92 @@
"glob": "^7.0.3",
"jasmine": "2.8.0",
"jasminewd2": "^2.1.0",
- "optimist": "~0.6.0",
"q": "1.4.1",
"saucelabs": "^1.5.0",
"selenium-webdriver": "3.6.0",
"source-map-support": "~0.4.0",
"webdriver-js-extender": "2.1.0",
- "webdriver-manager": "^12.0.6"
+ "webdriver-manager": "^12.1.7",
+ "yargs": "^15.3.1"
},
"dependencies": {
+ "@types/q": {
+ "version": "0.0.32",
+ "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
+ "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
"dev": true
},
+ "array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+ "dev": true,
+ "requires": {
+ "array-uniq": "^1.0.1"
+ }
+ },
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
- "ansi-styles": "^2.2.1",
- "escape-string-regexp": "^1.0.2",
- "has-ansi": "^2.0.0",
- "strip-ansi": "^3.0.0",
- "supports-color": "^2.0.0"
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
}
},
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
"del": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
@@ -11108,6 +32788,22 @@
"rimraf": "^2.2.8"
}
},
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
"globby": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
@@ -11122,10 +32818,75 @@
"pinkie-promise": "^2.0.0"
}
},
- "minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-path-cwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+ "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+ "dev": true
+ },
+ "is-path-in-cwd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+ "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+ "dev": true,
+ "requires": {
+ "is-path-inside": "^1.0.0"
+ }
+ },
+ "is-path-inside": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+ "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+ "dev": true,
+ "requires": {
+ "path-is-inside": "^1.0.1"
+ }
+ },
+ "jasmine": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz",
+ "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=",
+ "dev": true,
+ "requires": {
+ "exit": "^0.1.2",
+ "glob": "^7.0.6",
+ "jasmine-core": "~2.8.0"
+ }
+ },
+ "jasmine-core": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz",
+ "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
+ "dev": true
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"pify": {
@@ -11134,6 +32895,27 @@
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
+ "q": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
+ "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -11149,6 +32931,28 @@
"source-map": "^0.5.6"
}
},
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
@@ -11156,9 +32960,9 @@
"dev": true
},
"webdriver-manager": {
- "version": "12.1.1",
- "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.1.tgz",
- "integrity": "sha512-L9TEQmZs6JbMMRQI1w60mfps265/NCr0toYJl7p/R2OAk6oXAfwI6jqYP7EWae+d7Ad2S2Aj4+rzxoSjqk3ZuA==",
+ "version": "12.1.7",
+ "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.7.tgz",
+ "integrity": "sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA==",
"dev": true,
"requires": {
"adm-zip": "^0.4.9",
@@ -11173,24 +32977,84 @@
"semver": "^5.3.0",
"xml2js": "^0.4.17"
}
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
}
}
},
"proxy-addr": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
- "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
- "dev": true,
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
+ "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
- "ipaddr.js": "1.9.0"
+ "ipaddr.js": "1.9.1"
}
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
- "dev": true
+ "devOptional": true
},
"pseudomap": {
"version": "1.0.2",
@@ -11199,9 +33063,15 @@
"dev": true
},
"psl": {
- "version": "1.1.31",
- "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
- "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
+ "dev": true
+ },
+ "pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true
},
"public-encrypt": {
@@ -11216,13 +33086,20 @@
"parse-asn1": "^5.0.0",
"randombytes": "^2.0.1",
"safe-buffer": "^5.1.2"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ }
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
- "dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -11254,13 +33131,21 @@
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "dev": true
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+ },
+ "pupa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz",
+ "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==",
+ "dev": true,
+ "requires": {
+ "escape-goat": "^2.0.0"
+ }
},
"q": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
- "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
"dev": true
},
"qjobs": {
@@ -11270,10 +33155,9 @@
"dev": true
},
"qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
- "dev": true
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"query-string": {
"version": "4.3.4",
@@ -11298,9 +33182,9 @@
"dev": true
},
"querystringify": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
- "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"randombytes": {
@@ -11323,63 +33207,58 @@
}
},
"range-parser": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
- "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
- "dev": true
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
- "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
- "dev": true,
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
- "bytes": "3.0.0",
- "http-errors": "1.6.3",
- "iconv-lite": "0.4.23",
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
"unpipe": "1.0.0"
- }
- },
- "raw-loader": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-3.1.0.tgz",
- "integrity": "sha512-lzUVMuJ06HF4rYveaz9Tv0WRlUMxJ0Y1hgSkkgg+50iEdaI0TthyEDe08KIHb0XsF6rn8WYTqPCaGTZg3sX+qA==",
- "dev": true,
- "requires": {
- "loader-utils": "^1.1.0",
- "schema-utils": "^2.0.1"
},
"dependencies": {
- "ajv": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
- "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
- "dev": true
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
- "schema-utils": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
- "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
- "dev": true,
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
- "ajv": "^6.10.2",
- "ajv-keywords": "^3.4.1"
+ "safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
+ "raw-loader": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.1.tgz",
+ "integrity": "sha512-baolhQBSi3iNh1cglJjA0mYzga+wePk7vdEX//1dTFd+v4TsQlQE0jitJSNF1OIP82rdYulH7otaVmdlDaJ64A==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.5"
+ }
+ },
+ "rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dev": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ }
+ },
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -11410,21 +33289,104 @@
"npm-normalize-package-bin": "^1.0.0"
}
},
- "read-package-tree": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz",
- "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==",
+ "read-package-tree": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz",
+ "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==",
+ "dev": true,
+ "requires": {
+ "read-package-json": "^2.0.0",
+ "readdir-scoped-modules": "^1.0.0",
+ "util-promisify": "^2.1.0"
+ }
+ },
+ "read-pkg": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
+ "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
+ "dev": true,
+ "requires": {
+ "load-json-file": "^2.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^2.0.0"
+ },
+ "dependencies": {
+ "path-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
+ "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
+ "dev": true,
+ "requires": {
+ "pify": "^2.0.0"
+ }
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "read-pkg-up": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
+ "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
"dev": true,
"requires": {
- "read-package-json": "^2.0.0",
- "readdir-scoped-modules": "^1.0.0",
- "util-promisify": "^2.1.0"
+ "find-up": "^2.0.0",
+ "read-pkg": "^2.0.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "dev": true,
+ "requires": {
+ "locate-path": "^2.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "dev": true,
+ "requires": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "dev": true,
+ "requires": {
+ "p-try": "^1.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "dev": true,
+ "requires": {
+ "p-limit": "^1.1.0"
+ }
+ },
+ "p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+ "dev": true
+ }
}
},
"readable-stream": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
- "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
@@ -11449,14 +33411,12 @@
}
},
"readdirp": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
- "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+ "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
"dev": true,
"requires": {
- "graceful-fs": "^4.1.11",
- "micromatch": "^3.1.10",
- "readable-stream": "^2.0.2"
+ "picomatch": "^2.2.1"
}
},
"reflect-metadata": {
@@ -11466,33 +33426,33 @@
"dev": true
},
"regenerate": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
- "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz",
+ "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==",
"dev": true
},
"regenerate-unicode-properties": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz",
- "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==",
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz",
+ "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==",
"dev": true,
"requires": {
"regenerate": "^1.4.0"
}
},
"regenerator-runtime": {
- "version": "0.13.3",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
- "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==",
+ "version": "0.13.5",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
+ "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
"dev": true
},
"regenerator-transform": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz",
- "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==",
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz",
+ "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==",
"dev": true,
"requires": {
- "private": "^0.1.6"
+ "@babel/runtime": "^7.8.4"
}
},
"regex-not": {
@@ -11505,6 +33465,12 @@
"safe-regex": "^1.1.0"
}
},
+ "regex-parser": {
+ "version": "2.2.10",
+ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.10.tgz",
+ "integrity": "sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA==",
+ "dev": true
+ },
"regexp.prototype.flags": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
@@ -11516,26 +33482,47 @@
}
},
"regexpu-core": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
- "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz",
+ "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==",
+ "dev": true,
+ "requires": {
+ "regenerate": "^1.4.0",
+ "regenerate-unicode-properties": "^8.2.0",
+ "regjsgen": "^0.5.1",
+ "regjsparser": "^0.6.4",
+ "unicode-match-property-ecmascript": "^1.0.4",
+ "unicode-match-property-value-ecmascript": "^1.2.0"
+ }
+ },
+ "registry-auth-token": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz",
+ "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==",
+ "dev": true,
+ "requires": {
+ "rc": "^1.2.8"
+ }
+ },
+ "registry-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
+ "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
"dev": true,
"requires": {
- "regenerate": "^1.2.1",
- "regjsgen": "^0.2.0",
- "regjsparser": "^0.1.4"
+ "rc": "^1.2.8"
}
},
"regjsgen": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
- "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz",
+ "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==",
"dev": true
},
"regjsparser": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
- "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz",
+ "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==",
"dev": true,
"requires": {
"jsesc": "~0.5.0"
@@ -11568,9 +33555,9 @@
"dev": true
},
"request": {
- "version": "2.88.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
- "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+ "version": "2.88.2",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
@@ -11580,7 +33567,7 @@
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
- "har-validator": "~5.1.0",
+ "har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
@@ -11590,9 +33577,37 @@
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
- "tough-cookie": "~2.4.3",
+ "tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "dev": true
+ }
+ }
+ },
+ "request-promise-core": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
+ "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.19"
+ }
+ },
+ "request-promise-native": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz",
+ "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==",
+ "dev": true,
+ "requires": {
+ "request-promise-core": "1.1.4",
+ "stealthy-require": "^1.1.1",
+ "tough-cookie": "^2.3.3"
}
},
"require-directory": {
@@ -11602,9 +33617,9 @@
"dev": true
},
"require-main-filename": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
- "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"requires-port": {
@@ -11614,9 +33629,9 @@
"dev": true
},
"resolve": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
- "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+ "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
@@ -11643,6 +33658,114 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
+ "resolve-url-loader": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz",
+ "integrity": "sha512-K1N5xUjj7v0l2j/3Sgs5b8CjrrgtC70SmdCuZiJ8tSyb5J+uk3FoeZ4b7yTnH6j7ngI+Bc5bldHJIa8hYdu2gQ==",
+ "dev": true,
+ "requires": {
+ "adjust-sourcemap-loader": "2.0.0",
+ "camelcase": "5.3.1",
+ "compose-function": "3.0.3",
+ "convert-source-map": "1.7.0",
+ "es6-iterator": "2.0.3",
+ "loader-utils": "1.2.3",
+ "postcss": "7.0.21",
+ "rework": "1.0.1",
+ "rework-visit": "1.0.0",
+ "source-map": "0.6.1"
+ },
+ "dependencies": {
+ "emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true
+ },
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^2.0.0",
+ "json5": "^1.0.1"
+ }
+ },
+ "postcss": {
+ "version": "7.0.21",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz",
+ "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.2",
+ "source-map": "^0.6.1",
+ "supports-color": "^6.1.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "resp-modifier": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz",
+ "integrity": "sha1-sSTeXE+6/LpUH0j/pzlw9KpFa08=",
+ "dev": true,
+ "requires": {
+ "debug": "^2.2.0",
+ "minimatch": "^3.0.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "responselike": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+ "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
+ "dev": true,
+ "requires": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -11665,10 +33788,40 @@
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
"dev": true
},
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true
+ },
+ "rework": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz",
+ "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=",
+ "dev": true,
+ "requires": {
+ "convert-source-map": "^0.3.3",
+ "css": "^2.0.0"
+ },
+ "dependencies": {
+ "convert-source-map": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
+ "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
+ "dev": true
+ }
+ }
+ },
+ "rework-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz",
+ "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=",
+ "dev": true
+ },
"rfdc": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz",
- "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz",
+ "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==",
"dev": true
},
"rgb-regex": {
@@ -11684,9 +33837,9 @@
"dev": true
},
"rimraf": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
- "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
@@ -11703,24 +33856,25 @@
}
},
"rollup": {
- "version": "1.25.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.25.2.tgz",
- "integrity": "sha512-+7z6Wab/L45QCPcfpuTZKwKiB0tynj05s/+s2U3F2Bi7rOLPr9UcjUwO7/xpjlPNXA/hwnth6jBExFRGyf3tMg==",
+ "version": "2.10.9",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.10.9.tgz",
+ "integrity": "sha512-dY/EbjiWC17ZCUSyk14hkxATAMAShkMsD43XmZGWjLrgFj15M3Dw2kEkA9ns64BiLFm9PKN6vTQw8neHwK74eg==",
"dev": true,
"requires": {
- "@types/estree": "*",
- "@types/node": "*",
- "acorn": "^7.1.0"
+ "fsevents": "~2.1.2"
}
},
"run-async": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
- "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
- "dev": true,
- "requires": {
- "is-promise": "^2.1.0"
- }
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true
+ },
+ "run-parallel": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
+ "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
+ "dev": true
},
"run-queue": {
"version": "1.0.3",
@@ -11731,19 +33885,31 @@
"aproba": "^1.1.1"
}
},
+ "rx": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
+ "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=",
+ "dev": true
+ },
"rxjs": {
- "version": "6.5.4",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
- "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
+ "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
"requires": {
"tslib": "^1.9.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
+ }
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-regex": {
"version": "1.1.0",
@@ -11757,57 +33923,48 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass": {
- "version": "1.23.3",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.23.3.tgz",
- "integrity": "sha512-1DKRZxJMOh4Bme16AbWTyYeJAjTlrvw2+fWshHHaepeJfGq2soFZTnt0YhWit+bohtDu4LdyPoEj6VFD4APHog==",
+ "version": "1.26.5",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.5.tgz",
+ "integrity": "sha512-FG2swzaZUiX53YzZSjSakzvGtlds0lcbF+URuU9mxOv7WBh7NhXEVDa4kPKN4hN6fC2TkOTOKqiqp6d53N9X5Q==",
"dev": true,
"requires": {
"chokidar": ">=2.0.0 <4.0.0"
}
},
"sass-loader": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.0.tgz",
- "integrity": "sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz",
+ "integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==",
"dev": true,
"requires": {
"clone-deep": "^4.0.1",
"loader-utils": "^1.2.3",
"neo-async": "^2.6.1",
- "schema-utils": "^2.1.0",
+ "schema-utils": "^2.6.1",
"semver": "^6.3.0"
},
"dependencies": {
- "ajv": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
- "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "minimist": "^1.2.0"
}
},
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
- "dev": true
- },
- "schema-utils": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
- "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
- "ajv": "^6.10.2",
- "ajv-keywords": "^3.4.1"
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
}
},
"semver": {
@@ -11833,21 +33990,30 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
+ "saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dev": true,
+ "requires": {
+ "xmlchars": "^2.2.0"
+ }
+ },
"schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
- "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
+ "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"dev": true,
"requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
+ "@types/json-schema": "^7.0.4",
+ "ajv": "^6.12.2",
+ "ajv-keywords": "^3.4.1"
}
},
"scratch-blocks": {
- "version": "0.1.0-prerelease.1553681664",
- "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1553681664.tgz",
- "integrity": "sha512-W7mS0SLinu0T3dQlFtGc0jngQO7AeaN9VntsSVt7eQOHEG6Q28HnCT0g92Ti+pkwP1BxBHPljguJLrTKmm+sRQ==",
+ "version": "0.1.0-prerelease.20200512201140",
+ "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20200512201140.tgz",
+ "integrity": "sha512-lNTp5bxl/aHiN1I4bosEHj2MuiiKI8K714jgU8bUppWpNvQwp1PFRdoz/wjC8cjjReVBR37ZlkFQ5QzzJSULfA==",
"dev": true,
"requires": {
"exports-loader": "0.6.3",
@@ -11872,6 +34038,15 @@
"xml2js": "^0.4.17"
},
"dependencies": {
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
"tmp": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
@@ -11884,20 +34059,37 @@
}
},
"selfsigned": {
- "version": "1.10.7",
- "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
- "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
+ "version": "1.10.8",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
+ "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
"dev": true,
"requires": {
- "node-forge": "0.9.0"
+ "node-forge": "^0.10.0"
}
},
"semver": {
- "version": "5.5.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
- "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
+ "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
},
+ "semver-diff": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
+ "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
+ "dev": true,
+ "requires": {
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ }
+ }
+ },
"semver-dsl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz",
@@ -11905,6 +34097,14 @@
"dev": true,
"requires": {
"semver": "^5.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
}
},
"semver-intersect": {
@@ -11914,13 +34114,20 @@
"dev": true,
"requires": {
"semver": "^5.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
}
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
- "dev": true,
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -11937,56 +34144,36 @@
"statuses": "~1.5.0"
},
"dependencies": {
- "http-errors": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
- "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
- "dev": true,
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
- "depd": "~1.1.2",
- "inherits": "2.0.4",
- "setprototypeof": "1.1.1",
- "statuses": ">= 1.5.0 < 2",
- "toidentifier": "1.0.0"
+ "ms": "2.0.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
}
},
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
- },
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
- },
- "range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "dev": true
- },
- "setprototypeof": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
- "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
- "dev": true
- },
- "statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
- "dev": true
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serialize-javascript": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
- "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
- "dev": true
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
},
"serve-index": {
"version": "1.9.1",
@@ -12001,28 +34188,66 @@
"http-errors": "~1.6.2",
"mime-types": "~2.1.17",
"parseurl": "~1.3.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "dev": true,
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+ "dev": true
+ }
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
- "dev": true,
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
- },
- "dependencies": {
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "dev": true
- }
}
},
+ "server-destroy": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
+ "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=",
+ "dev": true
+ },
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -12065,10 +34290,9 @@
"dev": true
},
"setprototypeof": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
- "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
- "dev": true
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"sha.js": {
"version": "2.4.11",
@@ -12105,9 +34329,9 @@
"dev": true
},
"signal-exit": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
- "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
"simple-swizzle": {
@@ -12128,9 +34352,9 @@
}
},
"slash": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
- "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"smart-buffer": {
@@ -12155,6 +34379,15 @@
"use": "^3.1.0"
},
"dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
"define-property": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
@@ -12173,6 +34406,12 @@
"is-extendable": "^0.1.0"
}
},
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -12253,88 +34492,105 @@
}
},
"socket.io": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
- "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
+ "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==",
"dev": true,
"requires": {
- "debug": "~3.1.0",
- "engine.io": "~3.2.0",
+ "debug": "~4.1.0",
+ "engine.io": "~3.4.0",
"has-binary2": "~1.0.2",
"socket.io-adapter": "~1.1.0",
- "socket.io-client": "2.1.1",
- "socket.io-parser": "~3.2.0"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- }
+ "socket.io-client": "2.3.0",
+ "socket.io-parser": "~3.4.0"
}
},
"socket.io-adapter": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
- "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz",
+ "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==",
"dev": true
},
"socket.io-client": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
- "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz",
+ "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==",
"dev": true,
"requires": {
"backo2": "1.0.2",
"base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
"component-emitter": "1.2.1",
- "debug": "~3.1.0",
- "engine.io-client": "~3.2.0",
+ "debug": "~4.1.0",
+ "engine.io-client": "~3.4.0",
"has-binary2": "~1.0.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
- "socket.io-parser": "~3.2.0",
+ "socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
},
"dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+ "dev": true
+ },
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "socket.io-parser": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
+ "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
"dev": true,
"requires": {
- "ms": "2.0.0"
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
}
}
}
},
"socket.io-parser": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
- "integrity": "sha1-58Yii2qh+BTmFIrqMltRqpSZ4Hc=",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
+ "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
"dev": true,
"requires": {
"component-emitter": "1.2.1",
- "debug": "~3.1.0",
+ "debug": "~4.1.0",
"isarray": "2.0.1"
},
"dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
+ "component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+ "dev": true
},
"isarray": {
"version": "2.0.1",
@@ -12345,13 +34601,14 @@
}
},
"sockjs": {
- "version": "0.3.19",
- "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
- "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==",
+ "version": "0.3.20",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
+ "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==",
"dev": true,
"requires": {
"faye-websocket": "^0.10.0",
- "uuid": "^3.0.1"
+ "uuid": "^3.4.0",
+ "websocket-driver": "0.6.5"
}
},
"sockjs-client": {
@@ -12385,12 +34642,6 @@
"requires": {
"websocket-driver": ">=0.5.1"
}
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
}
}
},
@@ -12443,26 +34694,36 @@
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
- "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
- "dev": true
+ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
},
"source-map-loader": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz",
- "integrity": "sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.0.0.tgz",
+ "integrity": "sha512-ZayyQCSCrQazN50aCvuS84lJT4xc1ZAcykH5blHaBdVveSwjiFK8UGMPvao0ho54DTb0Jf7m57uRRG/YYUZ2Fg==",
"dev": true,
"requires": {
- "async": "^2.5.0",
- "loader-utils": "^1.1.0"
+ "data-urls": "^2.0.0",
+ "iconv-lite": "^0.5.1",
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.6",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
}
},
"source-map-resolve": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
- "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
"dev": true,
"requires": {
- "atob": "^2.1.1",
+ "atob": "^2.1.2",
"decode-uri-component": "^0.2.0",
"resolve-url": "^0.2.1",
"source-map-url": "^0.4.0",
@@ -12470,9 +34731,9 @@
}
},
"source-map-support": {
- "version": "0.5.9",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz",
- "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==",
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
@@ -12496,13 +34757,12 @@
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
- "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
- "dev": true
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
},
"spdx-correct": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
- "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
+ "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
"dev": true,
"requires": {
"spdx-expression-parse": "^3.0.0",
@@ -12510,15 +34770,15 @@
}
},
"spdx-exceptions": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
- "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
"dev": true
},
"spdx-expression-parse": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
- "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"requires": {
"spdx-exceptions": "^2.1.0",
@@ -12532,9 +34792,9 @@
"dev": true
},
"spdy": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz",
- "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
"dev": true,
"requires": {
"debug": "^4.1.0",
@@ -12542,23 +34802,6 @@
"http-deceiver": "^1.2.7",
"select-hose": "^2.0.0",
"spdy-transport": "^3.0.0"
- },
- "dependencies": {
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- }
}
},
"spdy-transport": {
@@ -12575,25 +34818,10 @@
"wbuf": "^1.7.3"
},
"dependencies": {
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
"readable-stream": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz",
- "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
@@ -12604,9 +34832,9 @@
}
},
"speed-measure-webpack-plugin": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.1.tgz",
- "integrity": "sha512-qVIkJvbtS9j/UeZumbdfz0vg+QfG/zxonAjzefZrqzkr7xOncLVXkeGbTpzd1gjCBM4PmVNkWlkeTVhgskAGSQ==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.3.tgz",
+ "integrity": "sha512-2ljD4Ch/rz2zG3HsLsnPfp23osuPBS0qPuz9sGpkNXTN1Ic4M+W9xB8l8rS8ob2cO4b1L+WTJw/0AJwWYVgcxQ==",
"dev": true,
"requires": {
"chalk": "^2.0.1"
@@ -12624,8 +34852,7 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
- "dev": true
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sshpk": {
"version": "1.16.1",
@@ -12645,30 +34872,12 @@
}
},
"ssri": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz",
- "integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz",
+ "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==",
"dev": true,
"requires": {
- "figgy-pudding": "^3.5.1",
"minipass": "^3.1.1"
- },
- "dependencies": {
- "minipass": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz",
- "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- }
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- }
}
},
"stable": {
@@ -12677,6 +34886,19 @@
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
"dev": true
},
+ "stack-generator": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz",
+ "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==",
+ "requires": {
+ "stackframe": "^1.1.1"
+ }
+ },
+ "stackframe": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
+ "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
+ },
"static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@@ -12699,9 +34921,14 @@
}
},
"statuses": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
- "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "stealthy-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
+ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
"dev": true
},
"stream-browserify": {
@@ -12743,42 +34970,43 @@
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"dev": true
},
+ "stream-throttle": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz",
+ "integrity": "sha1-rdV8jXzHOoFjDTHNVdOWHPr7qcM=",
+ "dev": true,
+ "requires": {
+ "commander": "^2.2.0",
+ "limiter": "^1.0.5"
+ }
+ },
"streamroller": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.3.tgz",
- "integrity": "sha512-P7z9NwP51EltdZ81otaGAN3ob+/F88USJE546joNq7bqRNTe6jc74fTBDyynxP4qpIfKlt/CesEYicuMzI0yJg==",
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz",
+ "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==",
"dev": true,
"requires": {
- "async": "^2.6.1",
- "date-format": "^2.0.0",
- "debug": "^3.1.0",
- "fs-extra": "^7.0.0",
- "lodash": "^4.17.10"
- },
- "dependencies": {
- "async": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
- "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
- "dev": true,
- "requires": {
- "lodash": "^4.17.11"
- }
+ "date-format": "^2.1.0",
+ "debug": "^4.1.1",
+ "fs-extra": "^8.1.0"
+ },
+ "dependencies": {
+ "date-format": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
+ "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==",
+ "dev": true
},
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
- "ms": "^2.1.1"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
}
- },
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
}
}
},
@@ -12788,44 +35016,61 @@
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"dev": true
},
- "string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "devOptional": true,
"requires": {
- "code-point-at": "^1.0.0",
- "is-fullwidth-code-point": "^1.0.0",
- "strip-ansi": "^3.0.0"
+ "safe-buffer": "~5.1.0"
}
},
- "string.prototype.trimleft": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
- "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
- "define-properties": "^1.1.3",
- "function-bind": "^1.1.1"
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
}
},
- "string.prototype.trimright": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
- "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
+ "string.prototype.trimend": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+ "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
- "function-bind": "^1.1.1"
+ "es-abstract": "^1.17.5"
}
},
- "string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "string.prototype.trimstart": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+ "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
"dev": true,
"requires": {
- "safe-buffer": "~5.1.0"
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
}
},
"strip-ansi": {
@@ -12837,50 +35082,32 @@
"ansi-regex": "^2.0.0"
}
},
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true
+ },
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true
+ },
"style-loader": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.0.0.tgz",
- "integrity": "sha512-B0dOCFwv7/eY31a5PCieNwMgMhVGFe9w+rh7s/Bx8kfFkrth9zfTZquoYvdw8URgiqxObQKcpW51Ugz1HjfdZw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz",
+ "integrity": "sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg==",
"dev": true,
"requires": {
- "loader-utils": "^1.2.3",
- "schema-utils": "^2.0.1"
- },
- "dependencies": {
- "ajv": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
- "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
- "dev": true
- },
- "schema-utils": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
- "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
- "dev": true,
- "requires": {
- "ajv": "^6.10.2",
- "ajv-keywords": "^3.4.1"
- }
- }
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.6.6"
}
},
"stylehacks": {
@@ -12895,12 +35122,12 @@
},
"dependencies": {
"postcss-selector-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
- "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
+ "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
"dev": true,
"requires": {
- "dot-prop": "^4.1.1",
+ "dot-prop": "^5.2.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1"
}
@@ -12932,6 +35159,12 @@
"ms": "2.0.0"
}
},
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -12949,12 +35182,34 @@
"loader-utils": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
"when": "~3.6.x"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ }
}
},
"supports-color": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
- "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
@@ -12987,6 +35242,12 @@
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
"dev": true
},
+ "symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
"tapable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
@@ -12994,32 +35255,72 @@
"dev": true
},
"tar": {
- "version": "4.4.8",
- "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
- "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
+ "version": "4.4.13",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
+ "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
- "minipass": "^2.3.4",
- "minizlib": "^1.1.1",
+ "minipass": "^2.8.6",
+ "minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
- "yallist": "^3.0.2"
+ "yallist": "^3.0.3"
},
"dependencies": {
+ "chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true
+ },
+ "fs-minipass": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
+ "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
+ "dev": true,
+ "requires": {
+ "minipass": "^2.6.0"
+ }
+ },
+ "minipass": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
+ "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
+ "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
+ "dev": true,
+ "requires": {
+ "minipass": "^2.9.0"
+ }
+ },
"yallist": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
- "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
}
}
},
+ "term-size": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz",
+ "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==",
+ "dev": true
+ },
"terser": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-4.5.1.tgz",
- "integrity": "sha512-lH9zLIbX8PRBEFCTvfHGCy0s9HEKnNso1Dx9swSopF3VUnFLB8DpQ61tHxoofovNC/sG0spajJM3EIIRSTByiQ==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz",
+ "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==",
"dev": true,
"requires": {
"commander": "^2.20.0",
@@ -13027,174 +35328,82 @@
"source-map-support": "~0.5.12"
},
"dependencies": {
- "commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true
- },
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
- },
- "source-map-support": {
- "version": "0.5.16",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
- "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
- "dev": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
}
}
},
"terser-webpack-plugin": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.3.tgz",
- "integrity": "sha512-gWHkaGzGYjmDoYxksFZynWTzvXOAjQ5dd7xuTMYlv4zpWlLSb6v0QLSZjELzP5dMs1ox30O1BIPs9dgqlMHuLQ==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.0.1.tgz",
+ "integrity": "sha512-eFDtq8qPUEa9hXcUzTwKXTnugIVtlqc1Z/ZVhG8LmRT3lgRY13+pQTnFLY2N7ATB6TKCHuW/IGjoAnZz9wOIqw==",
"dev": true,
"requires": {
- "cacache": "^13.0.1",
- "find-cache-dir": "^3.2.0",
- "jest-worker": "^25.1.0",
- "p-limit": "^2.2.2",
- "schema-utils": "^2.6.4",
- "serialize-javascript": "^2.1.2",
+ "cacache": "^15.0.3",
+ "find-cache-dir": "^3.3.1",
+ "jest-worker": "^26.0.0",
+ "p-limit": "^2.3.0",
+ "schema-utils": "^2.6.6",
+ "serialize-javascript": "^3.0.0",
"source-map": "^0.6.1",
- "terser": "^4.4.3",
+ "terser": "^4.6.13",
"webpack-sources": "^1.4.3"
},
"dependencies": {
- "ajv": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
- "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
- "dev": true
- },
- "find-cache-dir": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz",
- "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==",
- "dev": true,
- "requires": {
- "commondir": "^1.0.1",
- "make-dir": "^3.0.0",
- "pkg-dir": "^4.1.0"
- }
- },
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true
- },
- "jest-worker": {
- "version": "25.1.0",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.1.0.tgz",
- "integrity": "sha512-ZHhHtlxOWSxCoNOKHGbiLzXnl42ga9CxDr27H36Qn+15pQZd3R/F24jrmjDelw9j/iHUIWMWs08/u2QN50HHOg==",
- "dev": true,
- "requires": {
- "merge-stream": "^2.0.0",
- "supports-color": "^7.0.0"
- }
- },
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dev": true,
- "requires": {
- "p-locate": "^4.1.0"
- }
- },
- "make-dir": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz",
- "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==",
- "dev": true,
- "requires": {
- "semver": "^6.0.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true
- },
- "pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
- "dev": true,
- "requires": {
- "find-up": "^4.0.0"
- }
- },
- "schema-utils": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz",
- "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==",
+ "serialize-javascript": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
+ "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
"dev": true,
"requires": {
- "ajv": "^6.10.2",
- "ajv-keywords": "^3.4.1"
+ "randombytes": "^2.1.0"
}
},
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- },
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
+ }
+ }
+ },
+ "tfunk": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tfunk/-/tfunk-4.0.0.tgz",
+ "integrity": "sha512-eJQ0dGfDIzWNiFNYFVjJ+Ezl/GmwHaFTBTjrtqNPW0S7cuVDBrZrmzUz6VkMeCR4DZFqhd4YtLwsw3i2wYHswQ==",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.3",
+ "dlv": "^1.1.3"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
},
- "supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
- "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
- "has-flag": "^4.0.0"
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
}
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
}
}
},
@@ -13282,6 +35491,12 @@
}
}
},
+ "to-readable-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+ "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
+ "dev": true
+ },
"to-regex": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
@@ -13295,88 +35510,121 @@
}
},
"to-regex-range": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
- "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
- "is-number": "^3.0.0",
- "repeat-string": "^1.6.1"
+ "is-number": "^7.0.0"
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
- "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
- "dev": true
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
- "tough-cookie": {
- "version": "2.4.3",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
- "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+ "touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
"dev": true,
"requires": {
- "psl": "^1.1.24",
- "punycode": "^1.4.1"
+ "nopt": "~1.0.10"
},
"dependencies": {
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true
+ "nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
}
}
},
+ "tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ },
+ "tr46": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
+ "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.1"
+ }
+ },
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true
},
- "trim-right": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
- "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
- "dev": true
- },
"ts-node": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.0.3.tgz",
- "integrity": "sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA==",
+ "version": "8.10.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
+ "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
"dev": true,
"requires": {
"arg": "^4.1.0",
- "diff": "^3.1.0",
+ "diff": "^4.0.1",
"make-error": "^1.1.1",
- "source-map-support": "^0.5.6",
- "yn": "^3.0.0"
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
}
},
+ "ts-pnp": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
+ "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==",
+ "dev": true
+ },
"tslib": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
- "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
},
"tslint": {
- "version": "5.14.0",
- "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.14.0.tgz",
- "integrity": "sha512-IUla/ieHVnB8Le7LdQFRGlVJid2T/gaJe5VkjzRVSRR6pA2ODYrnfR1hmxi+5+au9l50jBwpbBL34txgv4NnTQ==",
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
+ "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
"dev": true,
"requires": {
- "babel-code-frame": "^6.22.0",
+ "@babel/code-frame": "^7.0.0",
"builtin-modules": "^1.1.1",
"chalk": "^2.3.0",
"commander": "^2.12.1",
- "diff": "^3.2.0",
+ "diff": "^4.0.1",
"glob": "^7.1.1",
- "js-yaml": "^3.7.0",
+ "js-yaml": "^3.13.1",
"minimatch": "^3.0.4",
- "mkdirp": "^0.5.1",
+ "mkdirp": "^0.5.3",
"resolve": "^1.3.2",
"semver": "^5.3.0",
- "tslib": "^1.8.0",
+ "tslib": "^1.13.0",
"tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
}
},
"tsutils": {
@@ -13386,6 +35634,14 @@
"dev": true,
"requires": {
"tslib": "^1.8.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
+ "dev": true
+ }
}
},
"tty-browserify": {
@@ -13409,20 +35665,34 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
+ "type": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+ "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
"type-fest": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
- "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
+ "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
"dev": true
},
"type-is": {
- "version": "1.6.16",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
- "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
- "dev": true,
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
- "mime-types": "~2.1.18"
+ "mime-types": "~2.1.24"
}
},
"typedarray": {
@@ -13431,45 +35701,59 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
+ "typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
+ "requires": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
"typescript": {
- "version": "3.7.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz",
- "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==",
+ "version": "3.9.7",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
+ "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
+ "dev": true
+ },
+ "ua-parser-js": {
+ "version": "0.7.21",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
+ "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==",
+ "dev": true
+ },
+ "ultron": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+ "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
"dev": true
},
- "uglify-js": {
- "version": "3.7.7",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.7.tgz",
- "integrity": "sha512-FeSU+hi7ULYy6mn8PKio/tXsdSXN35lm4KgV2asx00kzrLU9Pi3oAslcJT70Jdj7PHX29gGUPOT6+lXGBbemhA==",
+ "undefsafe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz",
+ "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==",
"dev": true,
- "optional": true,
"requires": {
- "commander": "~2.20.3",
- "source-map": "~0.6.1"
+ "debug": "^2.2.0"
},
"dependencies": {
- "commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
- "optional": true
+ "requires": {
+ "ms": "2.0.0"
+ }
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
}
}
},
- "ultron": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
- "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
- "dev": true
- },
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -13487,15 +35771,15 @@
}
},
"unicode-match-property-value-ecmascript": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz",
- "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz",
+ "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==",
"dev": true
},
"unicode-property-aliases-ecmascript": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz",
- "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz",
+ "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
"dev": true
},
"union-value": {
@@ -13510,6 +35794,14 @@
"set-value": "^2.0.1"
}
},
+ "unipointer": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/unipointer/-/unipointer-2.3.0.tgz",
+ "integrity": "sha512-m85sAoELCZhogI1owtJV3Dva7GxkHk2lI7A0otw3o0OwCuC/Q9gi7ehddigEYIAYbhkqNdri+dU1QQkrcBvirQ==",
+ "requires": {
+ "ev-emitter": "^1.0.1"
+ }
+ },
"uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
@@ -13540,6 +35832,15 @@
"imurmurhash": "^0.1.4"
}
},
+ "unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "dev": true,
+ "requires": {
+ "crypto-random-string": "^2.0.0"
+ }
+ },
"universal-analytics": {
"version": "0.4.20",
"resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.20.tgz",
@@ -13559,12 +35860,6 @@
"requires": {
"ms": "^2.1.1"
}
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
}
}
},
@@ -13577,8 +35872,7 @@
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
- "dev": true
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unquote": {
"version": "1.1.1",
@@ -13632,11 +35926,83 @@
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
"dev": true
},
+ "update-notifier": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.1.tgz",
+ "integrity": "sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg==",
+ "dev": true,
+ "requires": {
+ "boxen": "^4.2.0",
+ "chalk": "^3.0.0",
+ "configstore": "^5.0.1",
+ "has-yarn": "^2.1.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.3.1",
+ "is-npm": "^4.0.0",
+ "is-yarn-global": "^0.3.0",
+ "latest-version": "^5.0.0",
+ "pupa": "^2.0.1",
+ "semver-diff": "^3.1.1",
+ "xdg-basedir": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+ "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "dev": true,
+ "requires": {
+ "@types/color-name": "^1.1.1",
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
"uri-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
- "dev": true,
"requires": {
"punycode": "^2.1.0"
}
@@ -13675,36 +36041,51 @@
"requires-port": "^1.0.0"
}
},
+ "url-parse-lax": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+ "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
+ "dev": true,
+ "requires": {
+ "prepend-http": "^2.0.0"
+ },
+ "dependencies": {
+ "prepend-http": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
+ "dev": true
+ }
+ }
+ },
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
- "useragent": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
- "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==",
- "dev": true,
- "requires": {
- "lru-cache": "4.1.x",
- "tmp": "0.0.x"
- }
- },
"util": {
- "version": "0.11.1",
- "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
- "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
- "inherits": "2.0.3"
+ "inherits": "2.0.1"
+ },
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+ "dev": true
+ }
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "devOptional": true
},
"util-promisify": {
"version": "2.1.0",
@@ -13730,13 +36111,12 @@
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
- "dev": true
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
- "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
},
"validate-npm-package-license": {
@@ -13761,8 +36141,7 @@
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
- "dev": true
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vendors": {
"version": "1.0.4",
@@ -13793,22 +36172,113 @@
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
"dev": true
},
+ "w3c-hr-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+ "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+ "dev": true,
+ "requires": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "w3c-xmlserializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
+ "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
+ "dev": true,
+ "requires": {
+ "xml-name-validator": "^3.0.0"
+ }
+ },
"watchpack": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
- "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz",
+ "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==",
"dev": true,
"requires": {
- "chokidar": "^2.0.2",
+ "chokidar": "^3.4.1",
"graceful-fs": "^4.1.2",
- "neo-async": "^2.5.0"
+ "neo-async": "^2.5.0",
+ "watchpack-chokidar2": "^2.0.0"
+ }
+ },
+ "watchpack-chokidar2": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz",
+ "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "chokidar": "^2.1.8"
},
"dependencies": {
+ "anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ },
+ "dependencies": {
+ "normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "remove-trailing-separator": "^1.0.1"
+ }
+ }
+ }
+ },
+ "binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "dev": true,
+ "optional": true
+ },
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
"chokidar": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
"integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
"dev": true,
+ "optional": true,
"requires": {
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
@@ -13824,11 +36294,137 @@
"upath": "^1.1.1"
}
},
- "normalize-path": {
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "dev": true,
+ "optional": true
+ },
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ }
+ }
+ },
+ "is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "binary-extensions": "^1.0.0"
+ }
+ },
+ "is-number": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
}
}
},
@@ -13860,17 +36456,23 @@
"selenium-webdriver": "^3.0.1"
}
},
+ "webidl-conversions": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+ "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
+ "dev": true
+ },
"webpack": {
- "version": "4.41.2",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.2.tgz",
- "integrity": "sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==",
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz",
+ "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==",
"dev": true,
"requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-module-context": "1.8.5",
- "@webassemblyjs/wasm-edit": "1.8.5",
- "@webassemblyjs/wasm-parser": "1.8.5",
- "acorn": "^6.2.1",
+ "@webassemblyjs/ast": "1.9.0",
+ "@webassemblyjs/helper-module-context": "1.9.0",
+ "@webassemblyjs/wasm-edit": "1.9.0",
+ "@webassemblyjs/wasm-parser": "1.9.0",
+ "acorn": "^6.4.1",
"ajv": "^6.10.2",
"ajv-keywords": "^3.4.1",
"chrome-trace-event": "^1.0.2",
@@ -13881,44 +36483,49 @@
"loader-utils": "^1.2.3",
"memory-fs": "^0.4.1",
"micromatch": "^3.1.10",
- "mkdirp": "^0.5.1",
+ "mkdirp": "^0.5.3",
"neo-async": "^2.6.1",
"node-libs-browser": "^2.2.1",
"schema-utils": "^1.0.0",
"tapable": "^1.1.3",
- "terser-webpack-plugin": "^1.4.1",
- "watchpack": "^1.6.0",
+ "terser-webpack-plugin": "^1.4.3",
+ "watchpack": "^1.6.1",
"webpack-sources": "^1.4.1"
},
"dependencies": {
- "acorn": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
- "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==",
- "dev": true
- },
- "ajv": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
- "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
}
},
- "bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
- "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
- },
"cacache": {
- "version": "12.0.3",
- "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz",
- "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz",
+ "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
@@ -13938,12 +36545,35 @@
"y18n": "^4.0.0"
}
},
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+ "chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
"find-cache-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
@@ -13955,18 +36585,24 @@
"pkg-dir": "^3.0.0"
}
},
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"dev": true,
"requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
}
},
"is-wsl": {
@@ -13975,13 +36611,24 @@
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
"dev": true
},
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
- "yallist": "^3.0.2"
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
}
},
"memory-fs": {
@@ -13994,6 +36641,47 @@
"readable-stream": "^2.0.1"
}
},
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ }
+ },
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -14010,27 +36698,31 @@
}
},
"terser-webpack-plugin": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
- "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
+ "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
"dev": true,
"requires": {
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
- "serialize-javascript": "^2.1.2",
+ "serialize-javascript": "^4.0.0",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
"worker-farm": "^1.7.0"
}
},
- "yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
}
}
},
@@ -14058,23 +36750,17 @@
}
},
"mime": {
- "version": "2.4.4",
- "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
- "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
- "dev": true
- },
- "range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
+ "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==",
"dev": true
}
}
},
"webpack-dev-server": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.9.0.tgz",
- "integrity": "sha512-E6uQ4kRrTX9URN9s/lIbqTAztwEPdvzVrcmHE8EQ9YnuT9J8Es5Wrd8n9BKg1a0oZ5EgEke/EQFgUsp18dSTBw==",
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz",
+ "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==",
"dev": true,
"requires": {
"ansi-html": "0.0.7",
@@ -14085,33 +36771,89 @@
"debug": "^4.1.1",
"del": "^4.1.1",
"express": "^4.17.1",
- "html-entities": "^1.2.1",
+ "html-entities": "^1.3.1",
"http-proxy-middleware": "0.19.1",
"import-local": "^2.0.0",
"internal-ip": "^4.3.0",
"ip": "^1.1.5",
"is-absolute-url": "^3.0.3",
"killable": "^1.0.1",
- "loglevel": "^1.6.4",
+ "loglevel": "^1.6.8",
"opn": "^5.5.0",
"p-retry": "^3.0.1",
- "portfinder": "^1.0.25",
+ "portfinder": "^1.0.26",
"schema-utils": "^1.0.0",
"selfsigned": "^1.10.7",
"semver": "^6.3.0",
"serve-index": "^1.9.1",
- "sockjs": "0.3.19",
+ "sockjs": "0.3.20",
"sockjs-client": "1.4.0",
- "spdy": "^4.0.1",
+ "spdy": "^4.0.2",
"strip-ansi": "^3.0.1",
"supports-color": "^6.1.0",
"url": "^0.11.0",
"webpack-dev-middleware": "^3.7.2",
"webpack-log": "^2.0.0",
"ws": "^6.2.1",
- "yargs": "12.0.5"
+ "yargs": "^13.3.2"
},
"dependencies": {
+ "anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "requires": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ },
+ "dependencies": {
+ "normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "requires": {
+ "remove-trailing-separator": "^1.0.1"
+ }
+ }
+ }
+ },
+ "binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "dev": true
+ },
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
"chokidar": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
@@ -14132,13 +36874,55 @@
"upath": "^1.1.1"
}
},
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
"dev": true,
"requires": {
- "ms": "^2.1.1"
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "dev": true,
+ "optional": true
+ },
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ }
}
},
"is-absolute-url": {
@@ -14147,17 +36931,77 @@
"integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==",
"dev": true
},
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^1.0.0"
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ }
},
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
+ "schema-utils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.1.0",
+ "ajv-errors": "^1.0.0",
+ "ajv-keywords": "^3.1.0"
+ }
},
"semver": {
"version": "6.3.0",
@@ -14165,13 +37009,23 @@
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
- "ws": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
- "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+ "supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
- "async-limiter": "~1.0.0"
+ "has-flag": "^3.0.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
}
}
}
@@ -14193,14 +37047,6 @@
"dev": true,
"requires": {
"lodash": "^4.17.15"
- },
- "dependencies": {
- "lodash": {
- "version": "4.17.15",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
- "dev": true
- }
}
},
"webpack-sources": {
@@ -14222,31 +37068,66 @@
}
},
"webpack-subresource-integrity": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.3.4.tgz",
- "integrity": "sha512-6XbGYzjh30cGQT/NsC+9IAkJP8IL7/t47sbwR5DLSsamiD56Rwv4/+hsgEHsviPvrEFZ0JRAQtCRN3UsR2Pw9g==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.4.1.tgz",
+ "integrity": "sha512-XMLFInbGbB1HV7K4vHWANzc1CN0t/c4bBvnlvGxGwV45yE/S/feAXIm8dJsCkzqWtSKnmaEgTp/meyeThxG4Iw==",
"dev": true,
"requires": {
"webpack-sources": "^1.3.0"
}
},
"websocket-driver": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz",
- "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==",
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz",
+ "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=",
"dev": true,
"requires": {
- "http-parser-js": ">=0.4.0 <0.4.11",
- "safe-buffer": ">=5.1.0",
"websocket-extensions": ">=0.1.1"
}
},
"websocket-extensions": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
- "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "dev": true
+ },
+ "whatwg-encoding": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
+ "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
+ "dev": true,
+ "requires": {
+ "iconv-lite": "0.4.24"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ }
+ }
+ },
+ "whatwg-mimetype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
+ "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==",
"dev": true
},
+ "whatwg-url": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz",
+ "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==",
+ "dev": true,
+ "requires": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^2.0.2",
+ "webidl-conversions": "^5.0.0"
+ }
+ },
"when": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz",
@@ -14275,8 +37156,90 @@
"dev": true,
"requires": {
"string-width": "^1.0.2 || 2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "widest-line": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
+ "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
}
},
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
"worker-farm": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
@@ -14287,63 +37250,127 @@
}
},
"worker-plugin": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-3.2.0.tgz",
- "integrity": "sha512-W5nRkw7+HlbsEt3qRP6MczwDDISjiRj2GYt9+bpe8A2La00TmJdwzG5bpdMXhRt1qcWmwAvl1TiKaHRa+XDS9Q==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-4.0.3.tgz",
+ "integrity": "sha512-7hFDYWiKcE3yHZvemsoM9lZis/PzurHAEX1ej8PLCu818Rt6QqUAiDdxHPCKZctzmhqzPpcFSgvMCiPbtooqAg==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
+ "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ }
}
},
"wrap-ansi": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
- "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
- "string-width": "^1.0.1",
- "strip-ansi": "^3.0.1"
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
- "ws": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
- "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+ "write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
"dev": true,
"requires": {
- "async-limiter": "~1.0.0",
- "safe-buffer": "~5.1.0",
- "ultron": "~1.1.0"
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "ws": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
+ "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+ "devOptional": true,
+ "requires": {
+ "async-limiter": "~1.0.0"
}
},
+ "xdg-basedir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
+ "dev": true
+ },
+ "xhr2": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.0.tgz",
+ "integrity": "sha512-BDtiD0i2iKPK/S8OAZfpk6tyzEDnKKSjxWHcMBVmh+LuqJ8A32qXTyOx+TVOg2dKvq6zGBq2sgKPkEeRs1qTRA=="
+ },
+ "xml-name-validator": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
+ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
+ "dev": true
+ },
"xml2js": {
- "version": "0.4.19",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
- "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"dev": true,
"requires": {
"sax": ">=0.6.0",
- "xmlbuilder": "~9.0.1"
- },
- "dependencies": {
- "sax": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
- "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
- "dev": true
- }
+ "xmlbuilder": "~11.0.0"
}
},
"xmlbuilder": {
- "version": "9.0.7",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
- "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true
+ },
+ "xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
"xmlhttprequest-ssl": {
@@ -14356,7 +37383,37 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "dev": true
+ "devOptional": true
+ },
+ "y-leveldb": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.0.tgz",
+ "integrity": "sha512-sMuitVrsAUNh+0b66I42nAuW3lCmez171uP4k0ePcTAJ+c+Iw9w4Yq3wwiyrDMFXBEyQSjSF86Inc23wEvWnxw==",
+ "optional": true,
+ "requires": {
+ "level": "^6.0.1",
+ "lib0": "^0.2.31"
+ }
+ },
+ "y-protocols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.3.tgz",
+ "integrity": "sha512-2hSl0dqrD8Kph0SpvyakVYpKEnTLOLGIf7yvwmloQ4qS6RSvl6fUYHy6YocCvTvcd9MBuNeO4EqlmBcONJsvtw==",
+ "requires": {
+ "lib0": "^0.2.35"
+ }
+ },
+ "y-websocket": {
+ "version": "1.3.11",
+ "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-1.3.11.tgz",
+ "integrity": "sha512-Cvf85SE1mwFxrMRCokr4Rj16febCtfJziQWGn/F74h2W37SGPPpPNQjYZR9PFG7ryMAskoMF3ge7ZR1IEnL5CQ==",
+ "requires": {
+ "lib0": "^0.2.35",
+ "lodash.debounce": "^4.0.8",
+ "ws": "^6.2.1",
+ "y-leveldb": "^0.1.0",
+ "y-protocols": "^1.0.3"
+ }
},
"y18n": {
"version": "4.0.0",
@@ -14365,68 +37422,33 @@
"dev": true
},
"yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"yargs": {
- "version": "12.0.5",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
- "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
- "cliui": "^4.0.0",
- "decamelize": "^1.2.0",
+ "cliui": "^5.0.0",
"find-up": "^3.0.0",
- "get-caller-file": "^1.0.1",
- "os-locale": "^3.0.0",
+ "get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
- "require-main-filename": "^1.0.1",
+ "require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
- "string-width": "^2.0.0",
+ "string-width": "^3.0.0",
"which-module": "^2.0.0",
- "y18n": "^3.2.1 || ^4.0.0",
- "yargs-parser": "^11.1.1"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
- "dev": true,
- "requires": {
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^4.0.0"
- }
- },
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^3.0.0"
- }
- }
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
- "version": "11.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
- "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
@@ -14439,16 +37461,24 @@
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
"dev": true
},
+ "yjs": {
+ "version": "13.5.0",
+ "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.0.tgz",
+ "integrity": "sha512-fCBY8QIbQeXu8D6in4CBrdTCAmUsTHEgNXj27YnQDJMUQDNkXgvYV7vs1iiGekLoyBORt3/1qQa2cZqgvS8u8w==",
+ "requires": {
+ "lib0": "^0.2.35"
+ }
+ },
"yn": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/yn/-/yn-3.0.0.tgz",
- "integrity": "sha512-+Wo/p5VRfxUgBUGy2j/6KX2mj9AYJWOHuhMjMcbBFc3y54o9/4buK1ksBvuiK01C3kby8DH9lSmJdSxw+4G/2Q==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
},
"zone.js": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.2.tgz",
- "integrity": "sha512-UAYfiuvxLN4oyuqhJwd21Uxb4CNawrq6fPS/05Su5L4G+1TN+HVDJMUHNMobVQDFJRir2cLAODXwluaOKB7HFg=="
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.3.tgz",
+ "integrity": "sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg=="
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 8c3835fc..bc1c6a2b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,51 +10,79 @@
"build-prod": "ng build --prod",
"build-programaker": "ng build --prod -c programaker",
"test": "ng test",
+ "test-web": "ng test --watch=false",
+ "test-logic": "TS_NODE_COMPILER_OPTIONS='{\"experimentalDecorators\": true}' jasmine-ts",
+ "watch-test-logic": "nodemon --ext ts,js --exec jasmine-ts",
"lint": "ng lint",
- "e2e": "ng e2e"
+ "e2e": "ng e2e",
+ "gen-test-graphs": "ts-node -e 'this.describe = () => {}; require(\"./src/app/tests/logic/spreadsheet-compilation/_gen_graphs_and_tables.utils\").run().then(() => require(\"./src/app/tests/logic/flow-graph-analysis/_gen_graphs.utils\").run()).then(() => process.exit(0)).catch(() => process.exit(1))'",
+ "dev:ssr": "ng run programaker:serve-ssr",
+ "serve:ssr": "node dist/programaker/server/main.js",
+ "build:ssr": "ng build --prod && ng run programaker:server:production",
+ "build:programaker-ssr": "ng build --prod -c programaker && ng run programaker:server:programaker",
+ "prerender": "ng run programaker:prerender"
},
"private": true,
"dependencies": {
- "@angular/animations": "^9.0.0",
- "@angular/cdk": "^9.0.0",
- "@angular/common": "^9.0.0",
- "@angular/compiler": "^9.0.0",
- "@angular/core": "^9.0.0",
- "@angular/forms": "^9.0.0",
- "@angular/material": "~9.0.0",
- "@angular/platform-browser": "^9.0.0",
- "@angular/platform-browser-dynamic": "^9.0.0",
- "@angular/router": "^9.0.0",
- "bootstrap": "^4.3.1",
- "core-js": "^2.5.7",
- "ngx-bootstrap": "^5.4.0",
+ "@angular/animations": "^10.0.10",
+ "@angular/cdk": "^10.1.3",
+ "@angular/common": "^10.0.10",
+ "@angular/compiler": "^10.0.10",
+ "@angular/core": "^10.0.10",
+ "@angular/forms": "^10.0.10",
+ "@angular/material": "^10.1.3",
+ "@angular/platform-browser": "^10.0.10",
+ "@angular/platform-browser-dynamic": "^10.0.10",
+ "@angular/platform-server": "^10.0.10",
+ "@angular/router": "^10.0.10",
+ "@ng-toolkit/universal": "^8.1.0",
+ "@nguniversal/express-engine": "^10.0.2",
+ "@ngx-utils/cookies": "https://github.com/kenkeiras/ngx-cookies/releases/download/angular-10-support/ngx-cookies-angular-10.tgz",
+ "@types/cookie-parser": "^1.4.2",
+ "bootstrap": "^4.5.0",
+ "cookie-parser": "^1.4.5",
+ "core-js": "^2.6.11",
+ "express": "^4.15.2",
+ "fuse.js": "^5.2.3",
+ "huebee": "^2.1.0",
+ "jstz": "^2.1.1",
+ "ngx-bootstrap": "^5.6.1",
+ "ngx-toastr": "^13.2.0",
"nprogress": "^0.2.0",
- "rxjs": "^6.5.4",
- "zone.js": "~0.10.2"
+ "rxjs": "^6.5.5",
+ "y-websocket": "^1.3.11",
+ "yjs": "^13.5.0",
+ "zone.js": "^0.10.3"
},
"devDependencies": {
- "@angular-devkit/build-angular": "~0.900.1",
- "@angular/cli": "^9.0.1",
- "@angular/compiler-cli": "^9.0.0",
- "@types/jasmine": "3.3.12",
- "@types/node": "^11.12.0",
- "codelyzer": "^5.0.1",
+ "@angular-devkit/build-angular": "^0.1000.6",
+ "@angular/cli": "^10.0.6",
+ "@angular/compiler-cli": "^10.0.10",
+ "@nguniversal/builders": "^10.0.2",
+ "@types/express": "^4.17.0",
+ "@types/jasmine": "^3.5.10",
+ "@types/node": "^11.15.12",
+ "codelyzer": "^6.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
"google-closure-library": "^20190325.0.0",
- "jasmine-core": "~3.3.0",
- "jasmine-spec-reporter": "~4.2.1",
- "karma": "^4.0.1",
- "karma-chrome-launcher": "~2.2.0",
+ "jasmine": "^3.5.0",
+ "jasmine-core": "~3.5.0",
+ "jasmine-spec-reporter": "~5.0.0",
+ "jasmine-ts": "^0.3.0",
+ "karma": "~5.0.0",
+ "karma-chrome-launcher": "~3.1.0",
"karma-cli": "~2.0.0",
- "karma-coverage-istanbul-reporter": "^2.0.1",
- "karma-jasmine": "^2.0.1",
- "karma-jasmine-html-reporter": "^1.1.0",
+ "karma-coverage-istanbul-reporter": "~3.0.2",
+ "karma-jasmine": "~3.3.0",
+ "karma-jasmine-html-reporter": "^1.5.0",
"node-gyp": "^4.0.0",
- "protractor": "^5.4.2",
- "scratch-blocks": ">=0.1.0-prerelease.1553681664",
- "tar": "^4.4.8",
- "ts-node": "~8.0.3",
- "tslib": "^1.10.0",
- "tslint": "~5.14.0",
- "typescript": "^3.7.5"
+ "nodemon": "^2.0.4",
+ "protractor": "~7.0.0",
+ "scratch-blocks": "0.1.0-prerelease.20200512201140",
+ "tar": "^4.4.13",
+ "ts-node": "^8.10.1",
+ "tslib": "^2.0.0",
+ "tslint": "~6.1.0",
+ "typescript": "~3.9.7"
}
}
diff --git a/frontend/scripts/browser-test-ci-partial.dockerfile b/frontend/scripts/browser-test-ci-partial.dockerfile
new file mode 100644
index 00000000..af39e5c3
--- /dev/null
+++ b/frontend/scripts/browser-test-ci-partial.dockerfile
@@ -0,0 +1,5 @@
+FROM programakerproject/ci-base-frontend-browser:50e8c3f1a1fb0e7d24b7a42cf77421764e0f27e3
+
+# Change user to "tester". This will lift some restrictions on chromium
+RUN adduser -D tester
+USER tester
diff --git a/frontend/scripts/ci-partial-ssr.dockerfile b/frontend/scripts/ci-partial-ssr.dockerfile
new file mode 100644
index 00000000..bcfd73bd
--- /dev/null
+++ b/frontend/scripts/ci-partial-ssr.dockerfile
@@ -0,0 +1,22 @@
+FROM programakerproject/ci-base-frontend:50e8c3f1a1fb0e7d24b7a42cf77421764e0f27e3 as builder
+
+# Prepare dependencies
+ADD . /app
+RUN npm install . && make
+
+# Build application
+ARG BUILD_COMMAND=build:ssr
+RUN npm run ${BUILD_COMMAND}
+
+# Copy final app to runner
+FROM node:lts-alpine as runner
+
+COPY --from=builder /app/dist /app/dist
+
+WORKDIR app
+
+# Webserver port
+ENV PORT 80
+EXPOSE 80
+
+CMD ["node", "/app/dist/programaker/server/main.js"]
diff --git a/frontend/scripts/ci-partial.dockerfile b/frontend/scripts/ci-partial.dockerfile
index 451348b0..734e9987 100644
--- a/frontend/scripts/ci-partial.dockerfile
+++ b/frontend/scripts/ci-partial.dockerfile
@@ -1,4 +1,4 @@
-FROM plazaproject/ci-base-frontend:2813f6cb68f09389d78d537155793e844c2128b9 as builder
+FROM programakerproject/ci-base-frontend:50e8c3f1a1fb0e7d24b7a42cf77421764e0f27e3 as builder
# Prepare dependencies
ADD . /app
@@ -11,7 +11,7 @@ RUN npm run ${BUILD_COMMAND}
# Copy final app to runner
FROM nginx:alpine as runner
-copy --from=builder /app/dist/ /usr/share/nginx/html/
+copy --from=builder /app/dist/programaker/browser /usr/share/nginx/html/
# Add nginx configuration
ADD config/simple-nginx.conf /etc/nginx/conf.d/default.conf
diff --git a/frontend/scripts/launch-browser.sh b/frontend/scripts/launch-browser.sh
new file mode 100755
index 00000000..1fa90767
--- /dev/null
+++ b/frontend/scripts/launch-browser.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -eux
+
+exec "$BROWSER_BIN" $BROWSER_OPTS "$@"
diff --git a/frontend/scripts/run-browser-tests.sh b/frontend/scripts/run-browser-tests.sh
new file mode 100644
index 00000000..dfc76121
--- /dev/null
+++ b/frontend/scripts/run-browser-tests.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+set -eux
+
+npm install . && make
+
+COMMAND=${TEST_COMMAND:-test-web}
+export CHROME_BIN=`dirname "$0"`/launch-browser.sh
+
+export DISPLAY=:99
+
+Xvfb $DISPLAY &
+npm run $COMMAND
diff --git a/frontend/server.ts b/frontend/server.ts
new file mode 100644
index 00000000..537f0d9a
--- /dev/null
+++ b/frontend/server.ts
@@ -0,0 +1,73 @@
+import 'zone.js/dist/zone-node';
+
+import { ngExpressEngine } from '@nguniversal/express-engine';
+import * as express from 'express';
+import { join } from 'path';
+
+import { AppServerModule } from './src/main.server';
+import { APP_BASE_HREF } from '@angular/common';
+import { existsSync } from 'fs';
+import cookieParser from 'cookie-parser';
+import { environment } from 'environments/environment';
+
+// The Express app is exported so that it can be used by serverless Functions.
+export function app(): express.Express {
+ const server = express();
+ const distFolder = join(process.cwd(), 'dist/programaker/browser');
+ const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
+
+ server.use(cookieParser());
+
+ // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
+ server.engine('html', ngExpressEngine({
+ bootstrap: AppServerModule,
+ }));
+
+ server.set('view engine', 'html');
+ server.set('views', distFolder);
+
+ // Example Express Rest API endpoints
+ // server.get('/api/**', (req, res) => { });
+ // Serve static files from /browser
+ server.get('*.*', express.static(distFolder, {
+ maxAge: '1y'
+ }));
+
+ // All regular routes use the Universal engine
+ server.get('*', (req, res) => {
+ res.render(indexHtml, {
+ req,
+ res,
+ providers: [
+ { provide: APP_BASE_HREF, useValue: req.baseUrl },
+ { provide: 'RESPONSE', useValue: res },
+ { provide: 'REQUEST', useValue: req },
+ ] });
+ });
+
+ return server;
+}
+
+function run(): void {
+ const port = process.env.PORT || 4000;
+
+ // Start up the Node server
+ const server = app();
+ server.listen(port, () => {
+ console.log(`Node Express server listening on http://localhost:${port}`);
+
+ console.log("Environment:", environment);
+ });
+}
+
+// Webpack will replace 'require' with '__webpack_require__'
+// '__non_webpack_require__' is a proxy to Node 'require'
+// The below code is to ensure that the server is run only when not requiring the bundle.
+declare const __non_webpack_require__: NodeRequire;
+const mainModule = __non_webpack_require__.main;
+const moduleFilename = mainModule && mainModule.filename || '';
+if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
+ run();
+}
+
+export * from './src/main.server';
diff --git a/frontend/spec/conf.spec.js b/frontend/spec/conf.spec.js
new file mode 100644
index 00000000..cdbfe477
--- /dev/null
+++ b/frontend/spec/conf.spec.js
@@ -0,0 +1,20 @@
+const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
+
+jasmine.getEnv().clearReporters(); // remove default reporter logs
+jasmine.getEnv().addReporter(new SpecReporter({
+ suite: {
+ displayNumber: false,
+ },
+ spec: {
+ displayFailed: true,
+ displaySuccessful: false,
+ displayPending: true,
+ displayDuration: true,
+ displayStacktrace: 'none',
+ },
+ summary: {
+ displayErrorMessages: true,
+ displayStacktrace: 'raw',
+ }
+
+}));
diff --git a/frontend/spec/support/jasmine.json b/frontend/spec/support/jasmine.json
new file mode 100644
index 00000000..50b3666e
--- /dev/null
+++ b/frontend/spec/support/jasmine.json
@@ -0,0 +1,11 @@
+{
+ "spec_files": [
+ "spec/conf.spec.js",
+ "src/app/tests/logic/**/*[sS]pec.[jt]s"
+ ],
+ "helpers": [
+ "helpers/**/*.js"
+ ],
+ "stopSpecOnExpectationFailure": false,
+ "random": false
+}
diff --git a/frontend/src/app/HowToEnableServiceDialogComponent.ts b/frontend/src/app/HowToEnableServiceDialogComponent.ts
deleted file mode 100644
index ce233c72..00000000
--- a/frontend/src/app/HowToEnableServiceDialogComponent.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import { Component, Inject } from '@angular/core';
-import { ServiceEnableHowTo, ServiceEnableMessage, ServiceEnableEntry, ServiceEnableType } from './service';
-import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import { SessionService } from './session.service';
-import { ServiceService } from './service.service';
-
-@Component({
- selector: 'app-how-to-enable-service-dialog',
- templateUrl: 'how-to-enable-service-dialog.html',
- styleUrls: [
- 'how-to-enable-service-dialog.css',
- ],
- providers: [SessionService, ServiceService],
-})
-
-export class HowToEnableServiceDialogComponent {
- form = {};
- service: ServiceEnableHowTo;
- renderingZone: HTMLDivElement;
- type: ServiceEnableType;
-
- constructor(
- public dialogRef: MatDialogRef,
- public serviceService: ServiceService,
- @Inject(MAT_DIALOG_DATA)
- public data: ServiceEnableHowTo
- ) {
- this.service = data;
-
- dialogRef.afterOpened().subscribe(() => {
- this.renderingZone = (document
- .getElementById(dialogRef.id)
- .getElementsByClassName("rendering-zone")[0]) as HTMLDivElement;
-
- this.renderingZone.appendChild(this.render(data));
- });
- }
-
- render(data: ServiceEnableHowTo): HTMLElement {
- this.type = data.type;
-
- if (data.type === 'message') {
- return this.render_scripted_form(data as ServiceEnableMessage);
- }
-
- else if (data.type === 'form') {
- return this.render_scripted_form(data as ServiceEnableMessage);
- }
-
- throw new Error("Cannot render type: " + data.type);
- }
-
- render_scripted_form(data: ServiceEnableMessage): HTMLDivElement {
- const topMost = document.createElement("div");
-
- for (const entry of data.value.form) {
- topMost.appendChild(this.render_form_entry(entry));
- }
-
- return topMost;
- }
-
- render_form_entry(entry: ServiceEnableEntry): HTMLElement | Text {
- if (entry.type === 'text') {
- const element = document.createElement('span');
- element.classList.add('text');
- element.innerText = entry.value;
- return element;
- }
- else if (entry.type === 'console') {
- const element = document.createElement('div');
- element.classList.add('console');
- element.innerText = entry.value;
- return element;
- }
- else if (entry.type === 'tag') {
- let element;
- if (entry.tag === 'u') {
- element = document.createElement('u');
- }
- else if (entry.tag === 'console') {
- element = document.createElement('div');
- element.classList.add('console');
- }
- else if (entry.tag === 'a') {
- element = document.createElement('a');
- if ((entry.properties !== undefined) && (entry.properties.href !== undefined)) {
- element.setAttribute('href', entry.properties.href);
- }
-
- element.setAttribute('target', '_blank');
- element.setAttribute('rel', 'noopener noreferer');
-
- }
- else if (entry.tag === 'autolink') {
- element = document.createElement('a');
- /// @TODO: Complete functionality
- }
- else if (entry.tag === 'value') {
- element = document.createElement('span');
- /// @TODO: Complete functionality
- if (entry.properties !== undefined) {
- element.innerText = entry.properties.placeholder || '';
- }
- }
- else if (entry.tag === 'input') {
- element = document.createElement('input');
- if (entry.properties !== undefined) {
- const allowedProperties = ['type', 'placeholder', 'value', 'name'];
-
- for (const property of allowedProperties) {
- if (entry.properties[property] !== undefined) {
- element.setAttribute(property, entry.properties[property]);
- }
- }
-
- if (entry.properties.name) {
- this.input_controls_field(element, entry.properties.name);
- }
- }
- }
- else {
- throw new Error("Unknown tag: " + entry.tag);
- }
- for (const child of entry.content) {
- element.appendChild(this.render_form_entry(child));
- }
-
- return element;
- }
- }
-
- input_controls_field(entry: HTMLInputElement, fieldName: string) {
- const update_value = () => {
- this.form[fieldName] = entry.value;
- }
-
- entry.onchange = update_value;
- update_value();
- }
-
- send_form(): void {
- this.serviceService.registerService(this.data.metadata.service_id, this.form)
- .then((success) => {
- if (success) {
- this.dialogRef.close();
- }
- });
- }
-
- onNoClick(): void {
- this.dialogRef.close();
- }
-}
diff --git a/frontend/src/app/admin.service.ts b/frontend/src/app/admin.service.ts
new file mode 100644
index 00000000..793aa7ff
--- /dev/null
+++ b/frontend/src/app/admin.service.ts
@@ -0,0 +1,81 @@
+import { Injectable } from '@angular/core';
+import { User } from './user';
+
+import { SessionService } from './session.service';
+import { HttpClient } from '@angular/common/http';
+import { EnvironmentService } from './environment.service';
+
+export interface UserAdminData extends User {
+ status: string;
+ email: string;
+ registration_time: number;
+ last_active_time: number;
+}
+
+export interface PlatformUserStats {
+ count: number;
+ registered_last_day: number;
+ registered_last_week: number;
+ registered_last_month: number;
+ logged_last_hour: number;
+ logged_last_day: number;
+ logged_last_week: number;
+ logged_last_month: number;
+}
+
+export interface PlatformBridgeStats {
+ public_count: number;
+ private_count: number;
+ connections: number;
+ unique_connections: number;
+ messages_on_flight: number;
+}
+
+export interface PlatformStats {
+ active_services: {[key: string]: boolean};
+ bot_count: {active: number, workers: number};
+ thread_count: {active: number, workers: number};
+ monitor_count: {active: number, workers: number};
+ service_count: {all: number, public: number};
+ user_stats: PlatformUserStats;
+ bridge_stats: PlatformBridgeStats;
+};
+
+export interface PlatformStatsInfo {
+ stats: PlatformStats,
+ errors: string[],
+};
+
+@Injectable()
+export class AdminService {
+ constructor(
+ private http: HttpClient,
+ private sessionService: SessionService,
+ private environmentService: EnvironmentService,
+ ) {
+ this.http = http;
+ this.sessionService = sessionService;
+ }
+
+ private getListUsersUrl(): string {
+ return this.environmentService.getApiRoot() + '/users';
+ }
+
+ private getAdminStatsUrl(): string {
+ return this.environmentService.getApiRoot() + '/admin/stats';
+ }
+
+ async listAllUsers(): Promise {
+ const url = this.getListUsersUrl();
+ return (this.http.get(url,
+ { headers: this.sessionService.getAuthHeader() }
+ ).toPromise() as Promise);
+ }
+
+ async getStats(): Promise {
+ const url = this.getAdminStatsUrl();
+ return await (this.http.get(url,
+ { headers: this.sessionService.getAuthHeader() }
+ ).toPromise() as Promise);
+ }
+}
diff --git a/frontend/src/app/api-config.ts b/frontend/src/app/api-config.ts
deleted file mode 100644
index 3825fac6..00000000
--- a/frontend/src/app/api-config.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { environment } from '../environments/environment';
-
-const ApiHost = environment.ApiHost;
-const ApiRoot = ApiHost + '/api/v0';
-
-export { ApiHost, ApiRoot };
diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index d74fc478..0ecf5e64 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -1,30 +1,43 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
-
-import { BridgeIndexComponent } from './bridges/index.component';
-import { ServicesComponent } from './services.component';
-
-import { BridgeAddComponent } from './bridges/add.component';
-
-import { DashboardComponent } from './dashboard.component';
-
-import { ProgramsComponent } from './programs.component';
-import { ProgramDetailComponent } from './program-detail.component';
-
+import { AdminService } from './admin.service';
+import { DashboardComponent } from './dashboard/dashboard.component';
+import { FlowEditorComponent } from './flow-editor/flow-editor.component';
+import { GroupService } from './group.service';
+import { AboutPageComponent } from './info-pages/about-page.component';
+import { HomeRedirectComponent } from './info-pages/home-redirect.component';
import { LoginFormComponent } from './login-form/login-form.component';
-import { ResetPasswordStartComponent } from './login-form/reset-password-start.component';
-import { ResetPasswordUpdatePasswordComponent } from './login-form/reset-password-update-password.component';
import { RegisterFormComponent } from './login-form/register-form.component';
import { WaitForMailVerificationComponent } from './login-form/register-wait-for-mail-verification.component';
+import { ResetPasswordStartComponent } from './login-form/reset-password-start.component';
+import { ResetPasswordUpdatePasswordComponent } from './login-form/reset-password-update-password.component';
import { VerifyCodeComponent } from './login-form/verify-code.component';
+import { NewGroupComponent } from './new/group/new-group.component';
+import { ProgramDetailComponent } from './program-detail.component';
+import { ProgramService } from './program.service';
+import { AdminStatsResolver } from './resolvers/admin-stats.resolver';
+import { AdminUserListResolver } from './resolvers/admin-user-list.resolver';
+import { GroupInfoWithCollaboratorsResolver } from './resolvers/group-info-with-collaborators.resolver';
+import { ProgramListResolver } from './resolvers/program-list.resolver';
+import { RenderedAboutResolver } from './resolvers/rendered-about.resolver';
+import { SessionResolver } from './resolvers/session.resolver';
+import { SessionService } from './session.service';
+import { UserProfileResolver } from './resolvers/user-profile.resolver';
+import { AdminSettingsComponent } from './settings/admin-settings/admin-settings.component';
+import { GroupSettingsComponent } from './settings/group-settings/group-settings.component';
+import { SettingsComponent } from './settings/user-settings/settings.component';
+import { UserProfileComponent } from './profiles/user-profile.component';
+import { UserGroupsResolver } from './resolvers/user-groups.resolver';
+import { UserBridgesResolver } from './resolvers/user-bridges.resolver';
+import { SpreadsheetEditorComponent } from './program-editors/spreadsheet-editor/spreadsheet-editor.component';
+import { AuthorizeNewTokenComponent } from './components/authorize-new-token/authorize-new-token.component';
-import { HomeRedirectComponent } from './info-pages/home-redirect.component';
-import { AboutPageComponent } from './info-pages/about-page.component';
const routes: Routes = [
{ path: '', component: HomeRedirectComponent, pathMatch: 'full' },
- { path: 'about', component: AboutPageComponent },
+ { path: 'about', component: AboutPageComponent, resolve: { renderedAbout: RenderedAboutResolver } },
+ { path: 'authorize', component: AuthorizeNewTokenComponent, pathMatch: 'full' },
{ path: 'login', component: LoginFormComponent },
{ path: 'login/reset', component: ResetPasswordStartComponent },
{ path: 'login/reset/verify/:reset_verification_code', component: ResetPasswordUpdatePasswordComponent },
@@ -33,22 +46,56 @@ const routes: Routes = [
{ path: 'register/verify/:verification_code', component: VerifyCodeComponent },
// General
- { path: 'dashboard', component: DashboardComponent },
+ { path: 'dashboard', component: DashboardComponent, resolve: { programs: ProgramListResolver } },
+ { path: 'groups/:group_name', component: DashboardComponent, resolve: { programs: ProgramListResolver } },
+ { path: 'groups/:group_name/settings', component: GroupSettingsComponent, resolve: { groupInfo: GroupInfoWithCollaboratorsResolver, session: SessionResolver } },
+
+ // Profile pages
+ { path: 'users/:user_name', component: UserProfileComponent, resolve: { user_profile: UserProfileResolver } },
// Programs
- { path: 'users/:user_id/programs/', component: ProgramsComponent },
{ path: 'users/:user_id/programs/:program_id', component: ProgramDetailComponent },
-
- // Services
- { path: 'services', component: ServicesComponent },
+ { path: 'programs/:program_id/flow', component: FlowEditorComponent },
+ { path: 'programs/:program_id/scratch', component: ProgramDetailComponent },
+ { path: 'programs/:program_id/spreadsheet', component: SpreadsheetEditorComponent },
// Bridges
- { path: 'bridges', component: BridgeIndexComponent },
- { path: 'bridges/add', component: BridgeAddComponent },
+ { path: 'bridges', redirectTo: '/dashboard#bridges' },
+
+ // Settings
+ { path: 'settings', component: SettingsComponent, resolve: { session: SessionResolver,
+ groups: UserGroupsResolver,
+
+ user_profile: UserProfileResolver,
+ } },
+ { path: 'settings/admin', component: AdminSettingsComponent, resolve: { session: SessionResolver,
+ adminStats: AdminStatsResolver, userList: AdminUserListResolver } },
+
+ // Element creation
+ { path: 'new/group', component: NewGroupComponent },
+
+ // If no matching route found, go back to dashboard
+ { path: '**', component: DashboardComponent, resolve: { programs: ProgramListResolver } },
];
@NgModule({
- imports: [ RouterModule.forRoot(routes) ],
- exports: [ RouterModule ]
+ imports: [ RouterModule.forRoot(routes, { initialNavigation: 'enabled' }) ],
+ exports: [ RouterModule ],
+ providers: [
+ ProgramListResolver,
+ SessionResolver,
+ GroupInfoWithCollaboratorsResolver,
+ AdminStatsResolver,
+ AdminUserListResolver,
+ RenderedAboutResolver,
+ UserBridgesResolver,
+ UserGroupsResolver,
+ UserProfileResolver,
+
+ AdminService,
+ SessionService,
+ ProgramService,
+ GroupService,
+ ]
})
export class AppRoutingModule {}
diff --git a/frontend/src/app/app.browser.module.ts b/frontend/src/app/app.browser.module.ts
new file mode 100644
index 00000000..3d0e89e0
--- /dev/null
+++ b/frontend/src/app/app.browser.module.ts
@@ -0,0 +1,31 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { ANIMATION_MODULE_TYPE, BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { CookiesService } from '@ngx-utils/cookies';
+import { BrowserCookiesModule, BrowserCookiesService } from '@ngx-utils/cookies/browser';
+import { AppComponent } from './app.component';
+import { AppModule } from './app.module';
+
+import { ToastrModule } from 'ngx-toastr';
+
+@NgModule({
+ imports: [
+ BrowserModule.withServerTransition({ appId: 'serverApp' }),
+ BrowserCookiesModule.forRoot(),
+ BrowserAnimationsModule,
+ ToastrModule.forRoot(),
+ AppModule,
+ ],
+ bootstrap: [AppComponent],
+ providers: [
+ {
+ provide: CookiesService,
+ useClass: BrowserCookiesService,
+ },
+ {
+ provide: ANIMATION_MODULE_TYPE,
+ useValue: 'BrowserAnimations',
+ },
+ ],
+})
+export class AppBrowserModule {}
diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css
index 8da30bea..e76607f3 100644
--- a/frontend/src/app/app.component.css
+++ b/frontend/src/app/app.component.css
@@ -2,6 +2,23 @@
float: right;
}
+.login-indicator > .account-menu {
+ color: white;
+ background-color: transparent;
+ box-shadow: none;
+
+ width: max-content;
+ height: max-content;
+
+ display: inline-block;
+}
+
+.login-indicator .account-picture {
+ height: 2rem;
+ width: 2rem;
+ border-radius: 4px;
+}
+
a.action-link {
color: orange;
font-weight: bold;
@@ -22,60 +39,13 @@ a.action-link {
color: white;
}
-.bots {
- margin: 0 0 2em 0;
- list-style-type: none;
- padding: 0;
- width: 15em;
-}
-
-.bots li {
- cursor: pointer;
- position: relative;
- left: 0;
- background-color: #EEE;
- margin: .5em;
- padding: .3em 0;
- height: 1.6em;
- border-radius: 4px;
-}
-
-.bots li.selected:hover {
- background-color: #BBD8DC !important;
- color: white;
-}
-
-.bots li:hover {
- color: #607D8B;
- background-color: #DDD;
- left: .1em;
-}
-
-.bots .text {
- position: relative;
- top: -3px;
-}
-
-.bots .badge {
- display: inline-block;
- font-size: small;
- color: white;
- padding: 0.8em 0.7em 0 0.7em;
- background-color: #607D8B;
- line-height: 1em;
- position: relative;
- left: -1px;
- top: -4px;
- height: 1.8em;
- margin-right: .8em;
- border-radius: 4px 0 0 4px;
-}
-
.viewer {
margin: 0px;
}
#main-toolbar {
+ background-color: #27212e;
+ color: white;
padding-left: 0px;
}
@@ -91,9 +61,17 @@ a.action-link {
}
#main-menu-opener:hover {
- background-color: rgba(0,0,0, 0.3);
+ background-color: rgba(255,255,255, 0.3);
}
#main-menu-opener {
padding: 1em;
cursor: pointer;
}
+
+.app-content {
+ height: 100vh;
+}
+
+a[mat-menu-item] {
+ color: #272727;
+}
diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html
index 637f04cd..cb1bb94d 100644
--- a/frontend/src/app/app.component.html
+++ b/frontend/src/app/app.component.html
@@ -1,39 +1,44 @@
-
-
- {{title}}
+
+ {{title}}
-
-
-
Login
+
+
+
+
-
-
-
-
- Bots
-
-
-
-
- Programs
-
-
-
-
- Services
-
-
-
-
-
-
-
-
+
+
+
diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts
index c740bcd7..167aa2f3 100644
--- a/frontend/src/app/app.component.spec.ts
+++ b/frontend/src/app/app.component.spec.ts
@@ -1,32 +1,43 @@
-import { TestBed, async } from '@angular/core/testing';
-
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { CookiesService } from '@ngx-utils/cookies';
+import { BrowserCookiesModule, BrowserCookiesService } from '@ngx-utils/cookies/browser';
import { AppComponent } from './app.component';
+import { MatMenuModule } from '@angular/material/menu';
-describe('AppComponent', () => {
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [
- AppComponent
- ],
- }).compileComponents();
- }));
- it('should create the app', async(() => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.debugElement.componentInstance;
- expect(app).toBeTruthy();
- }));
+describe('AppComponent', () => {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ BrowserCookiesModule.forRoot(),
+ RouterTestingModule,
+ HttpClientTestingModule,
+ MatMenuModule,
+ ],
+ declarations: [
+ AppComponent
+ ],
+ providers: [
+ {
+ provide: CookiesService,
+ useClass: BrowserCookiesService,
+ },
+ ]
+ }).compileComponents();
+ }));
- it(`should have as title 'app works!'`, async(() => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.debugElement.componentInstance;
- expect(app.title).toEqual('app works!');
- }));
+ it('should create the app', async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app).toBeTruthy();
+ }));
- it('should render title in a h1 tag', async(() => {
- const fixture = TestBed.createComponent(AppComponent);
- fixture.detectChanges();
- const compiled = fixture.debugElement.nativeElement;
- expect(compiled.querySelector('h1').textContent).toContain('app works!');
- }));
+ it('should have an div.app-content', async(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector('div.app-content')).toBeTruthy();
+ }));
});
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts
index 96190157..504a35d5 100644
--- a/frontend/src/app/app.component.ts
+++ b/frontend/src/app/app.component.ts
@@ -1,8 +1,11 @@
-import { Component } from '@angular/core';
+import { ElementRef, ViewChild, Component, Inject, PLATFORM_ID } from '@angular/core';
import { Router } from '@angular/router';
-import { SessionService } from './session.service';
+import { SessionService, SessionInfoUpdate } from './session.service';
import { Session } from './session';
import { Subscription } from 'rxjs';
+import { getUserPictureUrl } from './utils';
+import { BrowserService } from './browser.service';
+import { EnvironmentService } from './environment.service';
@Component({
selector: 'app-my-app',
@@ -12,56 +15,63 @@ import { Subscription } from 'rxjs';
'libs/css/material-icons.css',
'libs/css/bootstrap.min.css',
],
- providers: [SessionService]
+ providers: [SessionService,]
})
-
export class AppComponent {
sessionSubscription: Subscription;
- username: string;
- loggedIn: boolean;
title = 'PrograMaker';
+ session: Session;
+
+ readonly _getUserPicture: (userId: string) => string;
constructor(
private router: Router,
- private session: SessionService
+ private browser: BrowserService,
+ private environmentService: EnvironmentService,
+
+ public sessionService: SessionService,
) {
+ this._getUserPicture = getUserPictureUrl.bind(this, environmentService);
+
this.router = router;
- this.session = session;
- this.loggedIn = false;
+ this.session = { active: false } as any;
- this.session.getSession().then((newSession: Session) => {
- if (newSession !== null) {
- this.loggedIn = newSession.active;
- if (newSession.active) {
- this.username = newSession.username;
- }
+ this.sessionService.getSessionMonitor().then((data) => {
+ if (data.session !== null) {
+ this.session = data.session;
}
+ data.monitor?.subscribe({
+ next: (update: SessionInfoUpdate) => {
+ this.session = update.session;
+ },
+ error: (error: any) => {
+ console.error("Error reading logs:", error);
+ },
+ complete: () => {
+ console.error("Session info data stopped")
+ }
+ });
+ }).catch(err => {
+ console.warn('Error monitoring session:', err);
});
- }
- gotoLogin(): void {
- this.router.navigate(['/login']);
+ this.browser.window.onresize = this.updateVerticalSpaces.bind(this);
+ this.browser.window.onload = this.updateVerticalSpaces.bind(this);
}
- logout(): void {
- this.session.logout();
- this.loggedIn = false;
- this.gotoLogin();
+ ngOnInit() {
}
- goHome(): void {
- this.gotoDashboard();
- }
+ updateVerticalSpaces(): void {
+ const height = this.browser.window.innerHeight;
+ const higherPart = document.getElementById('main-toolbar') as HTMLElement;
+ const lowerPart = document.getElementById('app-content') as HTMLElement;
- gotoDashboard(): void {
- this.router.navigate(['/dashboard']);
+ lowerPart.style.minHeight = (height - higherPart.clientHeight) + 'px';
}
- gotoPrograms(): void {
- this.router.navigate(['/programs']);
- }
-
- gotoServices(): void {
- this.router.navigate(['/services']);
+ logout(): void {
+ this.sessionService.logout();
+ this.router.navigate(['/login']);
}
}
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 1395dddf..2286b600 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -1,79 +1,106 @@
-import { BrowserModule } from '@angular/platform-browser';
-import { NgModule } from '@angular/core';
-import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
-
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatChipsModule } from '@angular/material/chips';
+import { MatRippleModule } from '@angular/material/core';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
-import { MatToolbarModule } from '@angular/material/toolbar';
-import { MatSidenavModule } from '@angular/material/sidenav';
-import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
-import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatMenuModule } from '@angular/material/menu';
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatDialogModule } from '@angular/material/dialog';
-import { MatTabsModule } from '@angular/material/tabs';
-import { MatChipsModule } from '@angular/material/chips';
-import { MatSnackBarModule } from '@angular/material/snack-bar'
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatRadioModule } from '@angular/material/radio';
+import { MatSelectModule } from '@angular/material/select';
+import { MatSidenavModule } from '@angular/material/sidenav';
+import { MatSlideToggleModule } from '@angular/material/slide-toggle';
+import { MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatToolbarModule } from '@angular/material/toolbar';
+import { MatTooltipModule } from '@angular/material/tooltip'
+import { MatStepperModule } from '@angular/material/stepper';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-
-import { AlertModule } from 'ngx-bootstrap';
-
+import { BrowserModule } from '@angular/platform-browser';
+import { AlertModule } from 'ngx-bootstrap/alert';
+import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
-import { DashboardComponent } from './dashboard.component';
-
-// Programs
-import { ProgramsComponent } from './programs.component';
-import { ProgramDetailComponent } from './program-detail.component';
-import { RenameProgramDialogComponent } from './RenameProgramDialogComponent';
-import { DeleteProgramDialogComponent } from './DeleteProgramDialogComponent';
-import { StopThreadProgramDialogComponent } from './StopThreadProgramDialogComponent';
-import { SetProgramTagsDialogComponent } from './program_tags/SetProgramTagsDialogComponent';
-
-// Services
-import { ServicesComponent } from './services.component';
-import { HowToEnableServiceDialogComponent } from './HowToEnableServiceDialogComponent';
-
-// Bridges
-import { BridgeIndexComponent } from './bridges/index.component';
-import { BridgeAddComponent } from './bridges/add.component';
import { BridgeDeleteDialogComponent } from './bridges/delete-dialog.component';
-
-// Custom signals
+import { GroupCollaboratorEditorComponent } from './components/group-collaborator-editor/group-collaborator-editor.component';
+import { AddConnectionDialogComponent } from './connections/add-connection-dialog.component';
import { CustomSignalCreateDialogComponent } from './custom_signals/create-dialog.component';
-
-// Templates
-import { TemplateCreateDialogComponent } from './templates/create-dialog.component';
-
+import { DashboardComponent } from './dashboard/dashboard.component';
+import { DeleteProgramDialogComponent } from './DeleteProgramDialogComponent';
+import { AddBridgeDialogComponent } from './dialogs/add-bridge-dialog/add-bridge-dialog.component';
+import { ConfirmDeleteDialogComponent } from './dialogs/confirm-delete-dialog/confirm-delete-dialog.component';
+import { EditCollaboratorsDialogComponent } from './dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component';
+import { UpdateBridgeDialogComponent } from './dialogs/update-bridge-dialog/update-bridge-dialog.component';
+import { ConfigureBlockDialogComponent } from './flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component';
+import { ConfigureFontColorDialogComponent } from './flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component';
+import { ConfigureLinkDialogComponent } from './flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component';
+import { FlowEditorComponent } from './flow-editor/flow-editor.component';
+import { AboutPageComponent } from './info-pages/about-page.component';
// Info
import { HomeRedirectComponent } from './info-pages/home-redirect.component';
-import { AboutPageComponent } from './info-pages/about-page.component';
-
-
-import { AppRoutingModule } from './app-routing.module';
import { LoginFormComponent } from './login-form/login-form.component';
-import { ResetPasswordStartComponent } from './login-form/reset-password-start.component';
-import { ResetPasswordUpdatePasswordComponent } from './login-form/reset-password-update-password.component';
import { RegisterFormComponent } from './login-form/register-form.component';
import { WaitForMailVerificationComponent } from './login-form/register-wait-for-mail-verification.component';
+import { ResetPasswordStartComponent } from './login-form/reset-password-start.component';
+import { ResetPasswordUpdatePasswordComponent } from './login-form/reset-password-update-password.component';
import { VerifyCodeComponent } from './login-form/verify-code.component';
-
-import { SummarizeJSON } from './summarize_json.filter';
+// Creation
+import { NewGroupComponent } from './new/group/new-group.component';
+// Programs
+import { ProgramDetailComponent } from './program-detail.component';
+import { SelectProgrammingModelDialogComponent } from './programs/select-programming-model-dialog/select-programming-model-dialog.component';
+import { SetProgramTagsDialogComponent } from './program_tags/SetProgramTagsDialogComponent';
+// Dialogs
+import { RenameProgramDialogComponent } from './RenameProgramDialogComponent';
import { SelectFromJSON } from './select_from_json.filter';
+// Services
+import { ServicesComponent } from './services.component';
+import { SessionService } from './session.service';
+import { AdminSettingsComponent } from './settings/admin-settings/admin-settings.component';
+import { GroupSettingsComponent } from './settings/group-settings/group-settings.component';
+import { SettingsComponent } from './settings/user-settings/settings.component';
+import { StopThreadProgramDialogComponent } from './StopThreadProgramDialogComponent';
+import { SummarizeJSON } from './summarize_json.filter';
+import { TemplateCreateDialogComponent } from './templates/create-dialog.component';
+import { ProgramService } from './program.service';
+import { UiSignalService } from './services/ui-signal.service';
+import { ConnectionService } from './connection.service';
+import { BridgeService } from './bridges/bridge.service';
+import { CustomBlockService } from './custom_block.service';
+import { CustomSignalService } from './custom_signals/custom_signal.service';
+import { MonitorService } from './monitor.service';
+import { ServiceService } from './service.service';
+import { TemplateService } from './templates/template.service';
+import { ChangeProgramVisilibityDialog } from './dialogs/change-program-visibility-dialog/change-program-visibility-dialog.component';
+import { CloneProgramDialogComponent } from './dialogs/clone-program-dialog/clone-program-dialog.component';
+import { AssetService } from './asset.service';
+import { UserProfileComponent } from './profiles/user-profile.component';
+import { ProfileService } from './profiles/profile.service';
+import { GroupProfileComponent } from './profiles/group-profile.component';
+import { ProgramEditorSidepanelComponent } from './components/program-editor-sidepanel/program-editor-sidepanel.component';
+import { SpreadsheetEditorComponent } from './program-editors/spreadsheet-editor/spreadsheet-editor.component';
+import { AuthorizeNewTokenComponent } from './components/authorize-new-token/authorize-new-token.component';
+import { ConnectToAvailableDialogComponent } from './dialogs/connect-to-available-dialog/connect-to-available-dialog.component';
+
+
@NgModule({
declarations: [
AppComponent,
DashboardComponent,
- ProgramsComponent,
+ SettingsComponent,
+ AdminSettingsComponent,
+ GroupSettingsComponent,
ProgramDetailComponent,
- BridgeIndexComponent,
- BridgeAddComponent,
+ FlowEditorComponent,
+ SpreadsheetEditorComponent,
ServicesComponent,
LoginFormComponent,
ResetPasswordStartComponent,
@@ -81,29 +108,51 @@ import { SelectFromJSON } from './select_from_json.filter';
RegisterFormComponent,
WaitForMailVerificationComponent,
VerifyCodeComponent,
+ UserProfileComponent,
+ GroupProfileComponent,
// Info pages
HomeRedirectComponent,
AboutPageComponent,
+ // Creation pages
+ NewGroupComponent,
+ AuthorizeNewTokenComponent,
+
// Dialogs
BridgeDeleteDialogComponent,
- HowToEnableServiceDialogComponent,
RenameProgramDialogComponent,
DeleteProgramDialogComponent,
CustomSignalCreateDialogComponent,
TemplateCreateDialogComponent,
StopThreadProgramDialogComponent,
SetProgramTagsDialogComponent,
+ AddConnectionDialogComponent,
+ EditCollaboratorsDialogComponent,
+ AddBridgeDialogComponent,
+ UpdateBridgeDialogComponent,
+ ConfirmDeleteDialogComponent,
+ ConfigureBlockDialogComponent,
+ ConfigureLinkDialogComponent,
+ ConfigureFontColorDialogComponent,
+ ChangeProgramVisilibityDialog,
+ CloneProgramDialogComponent,
+ ConnectToAvailableDialogComponent,
+
+ // Independent components
+ GroupCollaboratorEditorComponent,
+ ProgramEditorSidepanelComponent,
// Pipes
SummarizeJSON,
SelectFromJSON,
+ SelectProgrammingModelDialogComponent,
],
imports: [
MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
+ MatRadioModule,
MatIconModule,
MatToolbarModule,
MatSidenavModule,
@@ -116,22 +165,28 @@ import { SelectFromJSON } from './select_from_json.filter';
MatTabsModule,
MatChipsModule,
MatSnackBarModule,
+ MatRippleModule,
MatProgressSpinnerModule,
+ MatBadgeModule,
+ MatTooltipModule,
+ MatSelectModule,
+ MatStepperModule,
- BrowserAnimationsModule,
-
- BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
AlertModule.forRoot(),
AppRoutingModule,
+
+ BrowserModule.withServerTransition({ appId: 'serverApp' }),
],
- exports: [
- BridgeAddComponent,
+ exports: [AppComponent],
+ providers: [
+ AssetService, BridgeService, ConnectionService, CustomBlockService, CustomSignalService,
+ MonitorService, ProfileService, ProgramService, ServiceService, SessionService, TemplateService,
+ UiSignalService,
],
- providers: [],
- bootstrap: [AppComponent],
+ bootstrap: [],
})
export class AppModule {
diff --git a/frontend/src/app/app.server.module.ts b/frontend/src/app/app.server.module.ts
new file mode 100644
index 00000000..c9c536ab
--- /dev/null
+++ b/frontend/src/app/app.server.module.ts
@@ -0,0 +1,30 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { ANIMATION_MODULE_TYPE, NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { ServerModule } from '@angular/platform-server';
+import { CookiesService } from '@ngx-utils/cookies';
+import { ServerCookiesModule, ServerCookiesService } from '@ngx-utils/cookies/server';
+import { AppComponent } from './app.component';
+import { AppModule } from './app.module';
+
+@NgModule({
+ imports: [
+ NoopAnimationsModule,
+ BrowserModule.withServerTransition({appId: 'serverApp'}),
+ ServerModule,
+ ServerCookiesModule.forRoot(),
+ AppModule,
+ ],
+ bootstrap: [AppComponent],
+ providers: [
+ {
+ provide: CookiesService,
+ useClass: ServerCookiesService,
+ },
+ {
+ provide: ANIMATION_MODULE_TYPE,
+ useValue: 'NoopAnimations',
+ },
+ ],
+})
+export class AppServerModule {}
diff --git a/frontend/src/app/asset.service.ts b/frontend/src/app/asset.service.ts
new file mode 100644
index 00000000..9e52ccf5
--- /dev/null
+++ b/frontend/src/app/asset.service.ts
@@ -0,0 +1,38 @@
+import { HttpClient } from "@angular/common/http";
+import { Injectable } from "@angular/core";
+import { EnvironmentService } from "./environment.service";
+import { SessionService } from "./session.service";
+
+export interface TzItem {
+ country: string;
+ dst_offset: string;
+ latlong: string;
+ notes: string;
+ offset: string;
+ region: string;
+ status: "Alias" | "Canonical" | "Deprecated";
+ tz: string;
+}
+
+@Injectable()
+export class AssetService {
+ constructor(
+ private http: HttpClient,
+ private environmentService: EnvironmentService,
+ private sessionService: SessionService,
+ ) { }
+
+ public async getTimezoneData() : Promise {
+ const r = await this.http.get('/assets/timezones.json').toPromise();
+ return r as TzItem[];
+ }
+
+ public async copyAssetToProgram(sourceProgramId: string, assetId: string, destinationProgram: string): Promise {
+ const url = `${this.environmentService.getApiRoot()}/programs/by-id/${destinationProgram}/assets?copy_from=${sourceProgramId}/${assetId}`;
+ const formData = new FormData();
+
+ const result = await this.http.post(url, formData, {
+ headers: this.sessionService.getAuthHeader()
+ }).toPromise();
+ }
+}
diff --git a/frontend/src/app/blocks/BlockSynchronizer.ts b/frontend/src/app/blocks/BlockSynchronizer.ts
new file mode 100644
index 00000000..c1b05362
--- /dev/null
+++ b/frontend/src/app/blocks/BlockSynchronizer.ts
@@ -0,0 +1,255 @@
+///
+
+import { Synchronizer } from "app/syncronizer";
+import { ProgramEditorEventValue } from "app/program";
+import { Unsubscribable } from "rxjs";
+
+const CHECKPOINT_EVENT = 'save_checkpoint';
+const LEADER_WAIT_TIME = 60 * 1000;
+const FOLLOWER_WAIT_TIME = LEADER_WAIT_TIME * 2;
+
+export type BlocklyEvent = (
+ Blockly.Events.BlockCreate
+ | Blockly.Events.BlockChange
+ | Blockly.Events.BlockDelete
+ | Blockly.Events.BlockMove
+ | Blockly.Events.VarCreate
+ | Blockly.Events.VarDelete
+ | Blockly.Events.VarRename
+ | Blockly.Events.Ui
+) & { type: string }
+;
+
+const TYPE_BUFFER_SIZE = 10;
+type CircularBuffer = { values: T[], idx: number };
+
+export class BlockSynchronizer {
+ private readonly _typeBufferSize: number;
+ private readonly _recentCreatedBlocks: CircularBuffer;
+ private readonly _recentChangedBlocks: CircularBuffer<[string, string, string, any]>;
+ private readonly _recentDeletedBlocks: CircularBuffer;
+ private readonly _recentMovedBlocks: CircularBuffer<[string, string, string, {x: number, y: number}]>;
+
+ private readonly _recentCreatedVariables: CircularBuffer;
+ private readonly _recentDeletedVariables: CircularBuffer;
+ private readonly _recentRenamedVariables: CircularBuffer<[string, string]>;
+
+ private readonly _eventStream: Synchronizer;
+ private readonly _syncStatus: () => Promise;
+ private hasChanges: boolean = false;
+ private _timeout: NodeJS.Timeout;
+ eventSubscription: Unsubscribable;
+
+ constructor(eventStream: Synchronizer, syncStatus: () => Promise) {
+ this._typeBufferSize = TYPE_BUFFER_SIZE;
+ this._recentCreatedBlocks = { values: [], idx: 0 };
+ this._recentChangedBlocks = { values: [], idx: 0 };
+ this._recentDeletedBlocks = { values: [], idx: 0 };
+ this._recentMovedBlocks = { values: [], idx: 0 };
+
+ this._recentCreatedVariables = { values: [], idx: 0 };
+ this._recentDeletedVariables = { values: [], idx: 0 };
+ this._recentRenamedVariables = { values: [], idx: 0 };
+
+ this._eventStream = eventStream;
+ this._syncStatus = syncStatus;
+
+ this.eventSubscription = this._eventStream.subscribe({
+ next: this.onNewEvent.bind(this),
+ complete: () => {
+ clearTimeout(this._timeout)
+ },
+ error: () => {
+ clearTimeout(this._timeout)
+ },
+ });
+ this._timeout = setTimeout(this.onTimeoutCompleted.bind(this), FOLLOWER_WAIT_TIME);
+ }
+
+ private onNewEvent(ev: ProgramEditorEventValue) {
+ if (ev.type === CHECKPOINT_EVENT) {
+ console.debug("Someone else is syncing...");
+ clearTimeout(this._timeout);
+ this._timeout = setTimeout(this.onTimeoutCompleted.bind(this), FOLLOWER_WAIT_TIME);
+ }
+ else {
+ if (ev.save) {
+ this.hasChanges = true;
+ }
+ }
+ }
+
+ private async onTimeoutCompleted() {
+ console.debug("Syncing...");
+ this._eventStream.push({ type: CHECKPOINT_EVENT, save: false, value: {} });
+
+ if (!this.hasChanges) {
+ console.debug("Skipping due to no changes");
+
+ }
+ else {
+ try {
+ await this._syncStatus();
+ this.hasChanges = false;
+ }
+ catch(error) {
+ console.error("Sync error:", error);
+ }
+ }
+
+ this._timeout = setTimeout(this.onTimeoutCompleted.bind(this), LEADER_WAIT_TIME);
+ }
+
+ public receivedEvent(event: BlocklyEvent) {
+ const classif = this.classifyEvent(event);
+ this.hasChanges = true;
+
+ if (classif === null) {
+ return;
+ }
+ else if (!classif) {
+ // This shouldn't happen unless this type is not controlled
+ console.error(`Unexpected event type: ${event.type}`);
+ return;
+ }
+
+ const [ bucket, value, _comp ] = classif;
+ bucket.values[bucket.idx] = value;
+ bucket.idx = (bucket.idx + 1) % this._typeBufferSize;
+ }
+
+ public isDuplicated(event: BlocklyEvent): boolean {
+ const classif = this.classifyEvent(event);
+
+ if (classif === null) {
+ return false;
+ }
+ else if (['comment_create', 'comment_delete'].indexOf(event.type) >= 0) {
+ return true; // Just ignore it
+ }
+ else if (!classif) {
+ // This shouldn't happen unless this type is not controlled
+ console.error(`Unexpected event type: ${event.type}`);
+ this.hasChanges = true;
+ return false;
+ }
+
+ const [ bucket, value, comp ] = classif;
+ for (const existing of bucket.values) {
+ if (comp(existing, value)) {
+ return true;
+ }
+ }
+
+ this.hasChanges = true;
+ return false;
+ }
+
+ private classifyEvent(event: BlocklyEvent): [ CircularBuffer, any,
+ (x: any, y: any) => boolean
+ ] | null {
+ if (event instanceof Blockly.Events.BlockCreate) {
+ return [
+ this._recentCreatedBlocks,
+ event.blockId,
+ (x, y) => x === y,
+ ];
+ }
+ else if (event instanceof Blockly.Events.BlockChange) {
+ // This MIGHT not be necessary and probably all events can be passed
+ // as changes to the already found value are to be ignored.
+ return [
+ this._recentChangedBlocks,
+ [
+ event.blockId,
+ (event as any).element,
+ (event as any).name,
+ (event as any).newValue,
+ ],
+ ([a,b,c,d]: [any, any, any, any], [u,x,y,z]: [any, any, any, any]) => (
+ (a === u)
+ && (b === x)
+ && (c === y)
+ && (d === z)
+ )
+ ]
+ }
+ else if (event instanceof Blockly.Events.BlockDelete) {
+ // This MIGHT not be necessary and probably all events can be passed
+ // as deletions over non-existing blocks cannot happen.
+ return [
+ this._recentDeletedBlocks,
+ event.blockId,
+ (x, y) => x === y,
+ ]
+ }
+ else if (event instanceof Blockly.Events.BlockMove) {
+ return [
+ this._recentMovedBlocks,
+ [
+ event.blockId,
+ (event as any).newParentId,
+ (event as any).newInputName,
+ (event as any).newCoordinate,
+ ],
+ ([a,b,c,d]: [any, any, any, any], [u,x,y,z]: [any, any, any, any]) => (
+ (a === u)
+ && (b === x)
+ && (c === y)
+ && ((!d && !z)
+ || ((d.x === z.x)
+ && (d.y === z.y)))
+ )
+ ]
+ }
+ else if (event instanceof Blockly.Events.VarCreate) {
+ return [
+ this._recentCreatedVariables,
+ event.varId,
+ (x, y) => x === y,
+ ];
+ }
+ else if (event instanceof Blockly.Events.VarDelete) {
+ // This MIGHT not be necessary and probably all events can be passed
+ // as deletions over non-existing blocks cannot happen.
+ return [
+ this._recentDeletedVariables,
+ event.varId,
+ (x, y) => x === y,
+ ];
+ }
+ else if (event instanceof Blockly.Events.VarRename) {
+ return [
+ this._recentRenamedVariables,
+ [
+ event.varId,
+ (event as any).newName,
+ ],
+ ([a, b]: [string, string], [x, y]: [string, string]) => (
+ (a === x) && (b === y)
+ ),
+ ];
+ }
+ else if (event instanceof Blockly.Events.Ui) {
+ // UI events are not to be reflected anyway
+ return null;
+ }
+ else if (((event as any).type === 'endDrag')
+ || ((event as any).type === 'dragOutside')) {
+
+ // This might happen and is not bundled into Blockly.Events.Ui
+ return null;
+ }
+ }
+
+ public close() {
+ if (this.eventSubscription) {
+ this.eventSubscription.unsubscribe();
+ this.eventSubscription = null;
+ }
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+ }
+}
diff --git a/frontend/src/app/blocks/CallbackSequenceField.ts b/frontend/src/app/blocks/CallbackSequenceField.ts
new file mode 100644
index 00000000..196c0dd2
--- /dev/null
+++ b/frontend/src/app/blocks/CallbackSequenceField.ts
@@ -0,0 +1,159 @@
+///
+
+import { ResolvedDynamicSequenceBlockArgument } from "../custom_block";
+import { CustomBlockService } from "app/custom_block.service";
+
+export const TYPE = 'callback_sequence';
+
+declare const goog: any;
+
+export const SEQUENCE_SEPARATOR = '\\';
+
+function tagDepth(parent: string, list: [string, string][]): string[][] {
+ const newValues = [] as [string, string][];
+ for (const [name, key] of list) {
+ newValues.push([
+ name,
+ `${parent}${SEQUENCE_SEPARATOR}${key}`,
+ ]);
+ }
+
+ return newValues;
+}
+
+export function cleanCallbackSequenceValue(value: string): string {
+ const chunks = value.split(SEQUENCE_SEPARATOR);
+ return chunks[chunks.length - 1];
+}
+
+function sequenceValidator(customBlockService: CustomBlockService, block: ResolvedDynamicSequenceBlockArgument, field: Blockly.FieldDropdown): ((value: string) => void) {
+ return (value: string) => {
+ if (value === 'Select') {
+ return;
+ }
+
+ const foundName = ((field as any).menuGenerator_ as [string, string][]).find(([_x, y]) => y === value);
+ let selectedName = foundName ? foundName[0] : null;
+
+ if (selectedName && selectedName.match(/Go back [0-9]+ steps?/)) {
+ // TODO: Properly extract this name
+ selectedName = null;
+ }
+
+ const chunks = value ? value.split(SEQUENCE_SEPARATOR) : [];
+
+ const selectedDepth = chunks.length;
+ const selectedId = chunks[selectedDepth - 1];
+
+ if (selectedDepth >= block.callback_sequence.length) {
+ // Pull options from parent element.
+ // TODO: Deduplicate with the `else`
+ const depth = selectedDepth - 1;
+ const parentId = chunks[chunks.length - 2];
+
+ customBlockService.getCallbackOptionsOnSequence(block.program_id, block.bridge_id, block.callback_sequence[depth], parentId)
+ .then((options: [string, string][]) => {
+
+ const prelude = [ ["Back to Top", ''] ];
+ for (let i = 1; i < depth; i++ ) {
+ prelude.push([
+ `Go back ${i} step` + (i === 1 ? '' : 's'),
+ value.split(SEQUENCE_SEPARATOR, depth - i).join(SEQUENCE_SEPARATOR)
+ ]);
+ }
+
+ const menu = prelude.concat(tagDepth(value.split(SEQUENCE_SEPARATOR, depth).join(SEQUENCE_SEPARATOR), options));
+
+ (field as any).menuGenerator_ = menu;
+
+ if (field.value_ !== value) {
+ (field.setValue as any)(value, true);
+ }
+ else {
+ // As the value might not change, manually trigger an update
+ Blockly.Events.fire(new Blockly.Events.BlockChange(
+ field.sourceBlock_, 'field', field.name, field.value_, value));
+ }
+
+ (field.setValue as any)(value, true);
+ });
+
+ return;
+ }
+
+ if (selectedDepth === 0) {
+ (field as any).menuGenerator_ = block.first_level_options;
+ (field.setValue as any)('Select', true);
+ }
+ else {
+ customBlockService.getCallbackOptionsOnSequence(block.program_id, block.bridge_id, block.callback_sequence[selectedDepth], selectedId)
+ .then((options: [string, string][]) => {
+
+ const prelude = [ ["Back to Top", ''] ];
+ for (let i = 1; i < selectedDepth; i++ ) {
+ prelude.push([
+ `Go back ${i} step` + (i === 1 ? '' : 's'),
+ value.split(SEQUENCE_SEPARATOR, selectedDepth - i).join(SEQUENCE_SEPARATOR)
+ ]);
+ }
+
+ const newName = selectedName ? `Select in ${selectedName}` : 'Select';
+ prelude.push([newName, value]);
+ const menu = prelude.concat(tagDepth(value, options));
+
+ (field as any).menuGenerator_ = menu;
+
+ if (field.value_ !== value) {
+ (field.setValue as any)(value, true);
+ }
+ else {
+ // As the value might not change, manually trigger an update
+ Blockly.Events.fire(new Blockly.Events.BlockChange(
+ field.sourceBlock_, 'field', field.name, field.value_, value));
+ }
+ });
+ }
+ }
+}
+
+
+if (typeof goog !== 'undefined') {
+ // Goog cannot be used from SSR... we don't need it anyway
+ goog.provide('CallbackSequenceField');
+ goog.require('Blockly.Field');
+}
+
+const CallbackSequenceField = function(customBlockService: CustomBlockService, opt_value: ResolvedDynamicSequenceBlockArgument | any, opt_validator: any): any {
+ this.menuGenerator_ = (opt_value as ResolvedDynamicSequenceBlockArgument).first_level_options;
+ this.trimOptions_();
+
+ this.validator = sequenceValidator(customBlockService, opt_value, this);
+
+ // Call parent's constructor.
+ (Blockly.FieldDropdown as any).superClass_.constructor.call(this, '', opt_validator ? opt_validator : this.validator);
+ this.addArgType('dropdown');
+};
+
+if (typeof goog !== 'undefined') {
+ // Goog cannot be used from SSR... we don't need it anyway
+ goog.inherits(CallbackSequenceField, Blockly.FieldDropdown);
+}
+
+CallbackSequenceField.prototype.setValue = function(newValue: string, fromValidator: boolean) {
+ if (!fromValidator) {
+ this.validator(newValue);
+ }
+ else {
+ Blockly.FieldDropdown.prototype.setValue.call(this, newValue);
+ }
+}
+
+export function registerCallbackSequenceField(customBlockService: CustomBlockService) {
+
+ (CallbackSequenceField as any).fromJson = function(element: { options: ResolvedDynamicSequenceBlockArgument }): any {
+ // @ts-ignore
+ return new CallbackSequenceField(customBlockService, element.options, null);
+ };
+
+ (Blockly as any).Field.register(TYPE, CallbackSequenceField);
+}
diff --git a/frontend/src/app/blocks/CustomSignalController.ts b/frontend/src/app/blocks/CustomSignalController.ts
index 00f0a9b9..d40b85f6 100644
--- a/frontend/src/app/blocks/CustomSignalController.ts
+++ b/frontend/src/app/blocks/CustomSignalController.ts
@@ -6,7 +6,8 @@ import { alreadyRegisteredException, createDom } from './utils';
import { ToolboxController } from './ToolboxController';
import { CustomSignalService } from '../custom_signals/custom_signal.service';
import { CustomSignal } from '../custom_signals/custom_signal';
-declare const Blockly;
+import { ToolboxRegistrationHook } from './TemplateController';
+declare const Blockly : any;
export class CustomSignalController {
toolboxController: ToolboxController;
@@ -31,9 +32,6 @@ export class CustomSignalController {
}
register_custom_signal_blocks() {
- if ((!this.availableCustomSignals) || (this.availableCustomSignals.length === 0)) {
- return;
- }
if (this.registeredCustomSignalBlocks) {
return;
}
@@ -41,6 +39,9 @@ export class CustomSignalController {
this.registeredCustomSignalBlocks = true;
const availableCustomSignals = this.availableCustomSignals;
+ if (availableCustomSignals.length === 0) {
+ availableCustomSignals.push(["Unkown", '__plaza_internal_unlisted'])
+ }
// Create signal trigger block
Blockly.Blocks['automate_trigger_custom_signal'] = {
@@ -80,7 +81,6 @@ export class CustomSignalController {
triggerBlockArgValue.appendChild(triggerBlockArgValueShadow);
triggerBlock.appendChild(triggerBlockArgValue)
- this.customSignalsCategory.appendChild(triggerBlock);
// Create a signal receiver
Blockly.Blocks['automate_on_custom_signal'] = {
@@ -105,16 +105,19 @@ export class CustomSignalController {
}
};
- this.customSignalsCategory.appendChild(createDom('block',
- {
+ if (this.customSignalsCategory) {
+ this.customSignalsCategory.appendChild(createDom('block',
+ {
type: "automate_on_custom_signal",
id: "automate_on_custom_signal",
}));
+ this.customSignalsCategory.appendChild(triggerBlock);
+ }
this.toolboxController.update();
}
- injectBlocks(): Function[] {
+ injectBlocks(): ToolboxRegistrationHook[] {
try {
Blockly.Extensions.register('colours_custom_signals',
function () {
@@ -129,8 +132,10 @@ export class CustomSignalController {
}
return [
- (workspace) => {
- workspace.registerButtonCallback('AUTOMATE_CREATE_CUSTOM_SIGNAL', (x, y, z) => {
+ (workspace: Blockly.WorkspaceSvg) => {
+ this.register_custom_signal_blocks();
+
+ workspace.registerButtonCallback('AUTOMATE_CREATE_CUSTOM_SIGNAL', (b: Blockly.FlyoutButton) => {
this.create_custom_signal().then((custom_signal_name) => {
this.customSignalService.saveCustomSignal(custom_signal_name)
@@ -142,8 +147,11 @@ export class CustomSignalController {
console.log('Signal created');
+ if (this.availableCustomSignals[0][1] === '__plaza_internal_unlisted') {
+ this.availableCustomSignals.splice(0, 1);
+ }
+
this.availableCustomSignals.push([custom_signal_name, custom_signal_creation.id]);
- this.register_custom_signal_blocks();
this.toolboxController.update();
})
@@ -206,8 +214,6 @@ export class CustomSignalController {
this.availableCustomSignals.push([signal.name, signal.id]);
}
- this.register_custom_signal_blocks();
-
return cat;
});
}
diff --git a/frontend/src/app/blocks/TemplateController.ts b/frontend/src/app/blocks/TemplateController.ts
index 98fc5149..f0175635 100644
--- a/frontend/src/app/blocks/TemplateController.ts
+++ b/frontend/src/app/blocks/TemplateController.ts
@@ -4,7 +4,11 @@ import { MatDialog } from '@angular/material/dialog';
import { ToolboxController } from './ToolboxController';
import { TemplateService } from '../templates/template.service';
import { Template } from '../templates/template';
-declare const Blockly;
+import { EditorController } from '../program-editors/editor-controller';
+import { NgZone } from '@angular/core';
+
+export type ToolboxRegistrationHook = (workspace: any, editorController: EditorController, ngZone: NgZone) => void;
+declare const Blockly: any;
export class TemplateController {
toolboxController: ToolboxController;
@@ -29,16 +33,17 @@ export class TemplateController {
}
register_template_blocks() {
- if ((!this.availableTemplates) || (this.availableTemplates.length === 0)) {
- return;
- }
if (this.registeredTemplateBlocks) {
return;
}
this.registeredTemplateBlocks = true;
- const availableTemplates = this.availableTemplates;
+ const availableTemplates = this.availableTemplates || [];
+
+ if (availableTemplates.length === 0) {
+ availableTemplates.push(["Unkown", '__plaza_internal_unlisted'])
+ }
Blockly.Blocks['automate_match_template_check'] = {
init: function () {
@@ -62,12 +67,6 @@ export class TemplateController {
}
};
- this.templatesCategory.appendChild(createDom('block',
- {
- type: "automate_match_template_check",
- id: "automate_match_template_check",
- }));
-
Blockly.Blocks['automate_match_template_stmt'] = {
init: function () {
this.jsonInit({
@@ -90,16 +89,24 @@ export class TemplateController {
}
};
- this.templatesCategory.appendChild(createDom('block',
- {
+ if (this.templatesCategory) {
+ this.templatesCategory.appendChild(createDom('block',
+ {
+ type: "automate_match_template_check",
+ id: "automate_match_template_check",
+ }));
+
+ this.templatesCategory.appendChild(createDom('block',
+ {
type: "automate_match_template_stmt",
id: "automate_match_template_stmt",
}));
+ }
this.toolboxController.update();
}
- injectBlocks(): Function[] {
+ injectBlocks(): ToolboxRegistrationHook[] {
try {
Blockly.Extensions.register('colours_templates',
function () {
@@ -115,7 +122,9 @@ export class TemplateController {
return [
(workspace) => {
- workspace.registerButtonCallback('AUTOMATE_CREATE_TEMPLATE', (x, y, z) => {
+ this.register_template_blocks();
+
+ workspace.registerButtonCallback('AUTOMATE_CREATE_TEMPLATE', (b: Blockly.FlyoutButton) => {
this.create_template().then(([template_name, template_content]) => {
this.templateService.saveTemplate(template_name, template_content)
@@ -125,9 +134,11 @@ export class TemplateController {
return;
}
- this.availableTemplates.push([template_name, template_creation.id]);
- this.register_template_blocks();
+ if (this.availableTemplates[0][1] === '__plaza_internal_unlisted') {
+ this.availableTemplates.splice(0, 1);
+ }
+ this.availableTemplates.push([template_name, template_creation.id]);
this.toolboxController.update();
})
.catch(err => {
@@ -175,8 +186,6 @@ export class TemplateController {
this.availableTemplates.push([template.name, template.id]);
}
- this.register_template_blocks();
-
return cat;
});
}
diff --git a/frontend/src/app/blocks/Toolbox.ts b/frontend/src/app/blocks/Toolbox.ts
index 8b24fe58..ae18161e 100644
--- a/frontend/src/app/blocks/Toolbox.ts
+++ b/frontend/src/app/blocks/Toolbox.ts
@@ -7,18 +7,34 @@ import { MonitorMetadata } from '../monitor';
import { CustomSignalController } from './CustomSignalController';
import { CustomSignalService } from '../custom_signals/custom_signal.service';
import { CustomBlockService } from '../custom_block.service';
-import { CustomBlock, block_to_xml, get_block_category, get_block_toolbox_arguments, ResolvedCustomBlock, CategorizedCustomBlock, BridgeData } from '../custom_block';
+import { block_to_xml, get_block_category, get_block_toolbox_arguments,
+ ScratchBlockArgument, ResolvedCustomBlock, CategorizedCustomBlock, BridgeData } from '../custom_block';
import { ToolboxController } from './ToolboxController';
-import { TemplateController } from './TemplateController';
+import { TemplateController, ToolboxRegistrationHook } from './TemplateController';
import { TemplateService } from '../templates/template.service';
import { ServiceService } from '../service.service';
import { AvailableService } from '../service';
+import { SessionService } from '../session.service';
import { alreadyRegisteredException, createDom } from './utils';
+import { ConnectionService } from '../connection.service';
+import { BridgeConnection } from '../connection';
+import { iconDataToUrl } from '../utils';
+import { ProgramContent } from 'app/program';
+import { AssetService } from '../asset.service';
-declare const Blockly;
+
+import * as jstz from 'jstz';
+import { BridgeIndexData } from 'app/bridges/bridge';
+import { AddConnectionDialogComponent } from 'app/connections/add-connection-dialog.component';
+import { EditorController } from 'app/program-editors/editor-controller';
+import { NgZone } from '@angular/core';
+import { EnvironmentService } from 'app/environment.service';
+import { registerCallbackSequenceField } from './CallbackSequenceField';
+
+declare const Blockly: any;
const MonitorPrimaryColor = '#CC55CC';
const MonitorSecondaryColor = '#773377';
@@ -29,48 +45,136 @@ const CustomSecondaryColor = '#E7E7E7';
const TimeServiceUuid = "0093325b-373f-4f1c-bace-4532cce79df4";
const UtcTimeOfDayBlockId = `services.${TimeServiceUuid}.utc_is_day_of_week`;
+export type ToolboxRegistration = ((workspace: Blockly.WorkspaceSvg,
+ controller: EditorController,
+ ngZone: NgZone,
+ ) => void);
+
export class Toolbox {
- monitorService: MonitorService;
- customBlockService: CustomBlockService;
- dialog: MatDialog;
templateController: TemplateController;
customSignalController: CustomSignalController;
controller: ToolboxController;
- serviceService: ServiceService;
constructor(
- monitorService: MonitorService,
- customBlockService: CustomBlockService,
- dialog: MatDialog,
+ private program: ProgramContent,
+ private assetService: AssetService,
+ private monitorService: MonitorService,
+ private customBlockService: CustomBlockService,
+ private dialog: MatDialog,
templateService: TemplateService,
- serviceService: ServiceService,
+ private serviceService: ServiceService,
customSignalService: CustomSignalService,
+ private connectionService: ConnectionService,
+ private sessionService: SessionService,
+ private environmentService: EnvironmentService,
+ private readOnly: boolean,
+ toolboxController?: ToolboxController,
) {
- this.monitorService = monitorService;
- this.customBlockService = customBlockService;
- this.dialog = dialog;
- this.serviceService = serviceService;
-
- this.controller = new ToolboxController();
+ this.controller = toolboxController || new ToolboxController();
this.templateController = new TemplateController(this.dialog, this.controller, templateService);
this.customSignalController = new CustomSignalController(this.dialog, this.controller, customSignalService);
}
- async inject(): Promise<[HTMLElement, Function[], ToolboxController]> {
- const monitors = await this.monitorService.getMonitors();
- const custom_blocks = await this.customBlockService.getCustomBlocks();
- const services = await this.serviceService.getAvailableServices();
- this.controller.addCustomBlocks(custom_blocks);
+ async inject(): Promise<[HTMLElement, ToolboxRegistration[], ToolboxController]> {
+ let registrations: ToolboxRegistration[] = [];
+ let toolboxXML: HTMLElement;
+
+ if (this.readOnly) {
+ const [custom_blocks, services] = await Promise.all([
+ this.customBlockService.getCustomBlocksOnProgram(this.program.id, true),
+ this.serviceService.getAvailableServicesOnProgram(this.program.id),
+ ]);
+
+ this.controller.addCustomBlocks(custom_blocks);
+ const categorized_blocks = this.categorize_blocks(custom_blocks, services);
+
+ registrations = registrations.concat(await this.injectBlocks([], categorized_blocks, services, []));
+ toolboxXML = await this.injectToolbox([], categorized_blocks);
+ }
+ else {
+ const availableConnectionsQuery = this.connectionService.getAvailableBridgesForNewConnectionOnProgram(this.program.id);
+
+ const [monitors, custom_blocks, services, connections] = await Promise.all([
+ this.monitorService.getMonitorsOnProgram(this.program.id),
+ this.customBlockService.getCustomBlocksOnProgram(this.program.id, false),
+ this.serviceService.getAvailableServicesOnProgram(this.program.id),
+ this.connectionService.getConnectionsOnProgram(this.program.id),
+ ]) as [MonitorMetadata[], ResolvedCustomBlock[], AvailableService[], BridgeConnection[]];
- const categorized_blocks = this.categorize_blocks(custom_blocks,services);
+ this.controller.addCustomBlocks(custom_blocks);
+ const categorized_blocks = this.categorize_blocks(custom_blocks,services);
+
+ let availableConnections: BridgeIndexData[];
+ try {
+ availableConnections = await availableConnectionsQuery;
+ }
+ catch (error) {
+ if ((error.name === 'HttpErrorResponse') && (error.status === 401)) {
+ // User cannot add connections, so skip adding them
+ availableConnections = [];
+ }
+ else {
+ throw error;
+ }
+ }
+
+ registrations = registrations.concat(await this.injectBlocks(monitors, categorized_blocks, services, connections));
+ toolboxXML = await this.injectToolbox(monitors, categorized_blocks);
+
+ registrations = registrations.concat(this.addAvailableConnections(availableConnections, toolboxXML));
+ }
- const registrations = this.injectBlocks(monitors, categorized_blocks, services);
- const toolboxXML = await this.injectToolbox(monitors, categorized_blocks);
this.controller.setToolbox(toolboxXML);
return [toolboxXML, registrations, this.controller];
}
+ addAvailableConnections(availableBridges: BridgeIndexData[], toolboxXML: HTMLElement): ToolboxRegistrationHook[] {
+ const hooks: ToolboxRegistrationHook[] = [];
+ for (const bridge of availableBridges) {
+ const category = createDom('category', {
+ name: bridge.name,
+ colour: Toolbox.get_bridge_color(bridge.id),
+ secondaryColour: Toolbox.get_bridge_secondary_color(bridge.id),
+ id: bridge.id,
+ })
+
+ const callbackKey = "AUTOMATE_CONNECT_" + bridge.id;
+ category.appendChild(createDom('button', {
+ text: "Connect to " + bridge.name,
+ callbackKey: callbackKey,
+ }));
+
+ toolboxXML.appendChild(category);
+
+ hooks.push((workspace: Blockly.WorkspaceSvg, editorController: EditorController, ngZone: NgZone) => {
+
+ workspace.registerButtonCallback(callbackKey, (_x: Blockly.FlyoutButton) => {
+ ngZone.run(() => {
+ const dialogRef = this.dialog.open(AddConnectionDialogComponent, {
+ disableClose: false,
+ data: {
+ programId: this.program.id,
+ bridgeInfo: bridge,
+ }
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ editorController.reloadToolbox();
+ });
+ });
+ });
+ });
+ }
+
+ return hooks;
+ }
+
categorize_blocks(custom_blocks: ResolvedCustomBlock[], services: AvailableService[]): CategorizedCustomBlock[]{
const categorized_blocks: CategorizedCustomBlock[] = [];
for (const service of services) {
@@ -96,20 +200,56 @@ export class Toolbox {
return categorized_blocks;
}
- injectBlocks(monitors: MonitorMetadata[], custom_blocks: CategorizedCustomBlock[], services: AvailableService[]): Function[] {
- let registrations = [];
+ async injectBlocks(monitors: MonitorMetadata[],
+ custom_blocks: CategorizedCustomBlock[],
+ services: AvailableService[],
+ connections: BridgeConnection[],
+ ): Promise {
+ let registrations: ToolboxRegistrationHook[] = [];
+ registerCallbackSequenceField(this.customBlockService);
this.injectMonitorBlocks(monitors);
- this.injectTimeBlocks();
+ this.injectCustomControls();
+ await this.injectTimeBlocks();
this.injectJSONBlocks();
+ this.injectListBlocks();
+ this.injectOperationBlocks(connections);
registrations = registrations.concat(this.templateController.injectBlocks());
registrations = registrations.concat(this.customSignalController.injectBlocks());
- this.injectCustomBlocks(custom_blocks);
+ this.injectCustomBlocks(custom_blocks, connections);
+ this.patchBlocks();
return registrations;
}
- injectTimeBlocks() {
+ patchBlocks() {
+ // Original: https://github.com/LLK/scratch-blocks/blob/d9e7bb7c497d23f55cc06103837ad448d786714b/blocks_vertical/data.js#L80
+ Blockly.Blocks['data_changevariableby'] = {
+ /**
+ * Block to change variable by a certain value
+ * @this Blockly.Block
+ */
+ init: function() {
+ this.jsonInit({
+ "message0": "increment %1 by %2",
+ "args0": [
+ {
+ "type": "field_variable",
+ "name": "VARIABLE"
+ },
+ {
+ "type": "input_value",
+ "name": "VALUE"
+ }
+ ],
+ "category": Blockly.Categories.data,
+ "extensions": ["colours_data", "shape_statement"]
+ });
+ }
+ };
+ }
+
+ injectTimeBlocks(): Promise {
Blockly.Blocks['time_trigger_at'] = {
init: function () {
this.jsonInit({
@@ -174,6 +314,163 @@ export class Toolbox {
}
};
+
+ const timeReady = this.assetService.getTimezoneData().then((tz) => {
+
+ const detectedTz = jstz.determine().name();
+ console.log("Autodetected Timezone:", detectedTz);
+
+ const tzData = (tz
+ .filter(v => v.status === 'Canonical' || v.status === 'Alias')
+ .sort((a, b) => {
+ if (a.tz > b.tz) {
+ return 1;
+ }
+ if (a.tz < b.tz) {
+ return -1;
+ }
+ return 0;
+ })
+ .map((v) => {
+ return [v.tz, v.tz]
+ }));
+
+ // Add the detected timezone at the top
+ tzData.unshift([detectedTz, detectedTz]);
+
+ const tzArg = {
+ 'type': 'field_dropdown',
+ 'name': 'TIMEZONE4',
+ 'options': tzData
+ };
+
+
+ Blockly.Blocks['time_trigger_at_tz'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_trigger_at',
+ 'message0': 'Every day at %1:%2:%3 in %4',
+ 'args0': [
+ {
+ 'type': 'input_value',
+ 'name': 'HOUR1'
+ },
+ {
+ 'type': 'input_value',
+ 'name': 'MINUTE2'
+ },
+ {
+ 'type': 'input_value',
+ 'name': 'SECOND3'
+ },
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'shape_hat']
+ });
+ }
+ };
+
+ Blockly.Blocks['time_get_tz_hour'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_get_tz_hour',
+ 'message0': 'Hour at %1',
+ 'args0': [
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'output_number']
+ });
+ }
+ };
+
+ Blockly.Blocks['time_get_tz_minute'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_get_tz_minute',
+ 'message0': 'Minutes at %1',
+ 'args0': [
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'output_number']
+ });
+ }
+ };
+
+ Blockly.Blocks['time_get_tz_seconds'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_get_tz_seconds',
+ 'message0': 'Seconds at %1',
+ 'args0': [
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'output_number']
+ });
+ }
+ };
+
+ Blockly.Blocks['time_get_tz_day_of_month'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_get_tz_day_of_month',
+ 'message0': 'Day of the month at %1',
+ 'args0': [
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'output_number']
+ });
+ }
+ };
+
+ Blockly.Blocks['time_get_tz_month_of_year'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_get_tz_month_of_hear',
+ 'message0': 'Month of the year at %1 (January=1, Februrary=2, ...)',
+ 'args0': [
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'output_number']
+ });
+ }
+ };
+
+ Blockly.Blocks['time_get_tz_year'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_get_tz_year',
+ 'message0': 'Current year (at %1)',
+ 'args0': [
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'output_number']
+ });
+ }
+ };
+
+ Blockly.Blocks['time_get_tz_day_of_week'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'time_get_tz_day_of_week',
+ 'message0': 'Numeric day of week at %1 (Monday=1, Tuesday=2, ...)',
+ 'args0': [
+ tzArg,
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_time', 'output_number']
+ });
+ }
+ };
+
+ });
+
Blockly.Blocks[UtcTimeOfDayBlockId] = {
init: function () {
this.jsonInit({
@@ -215,6 +512,81 @@ export class Toolbox {
throw e;
}
}
+
+ return timeReady;
+ }
+
+ injectListBlocks() {
+ Blockly.Blocks['data_setlistto'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'data_setlistto',
+ 'message0': 'set list %1 to %2',
+ "args0": [
+ {
+ "type": "field_variable",
+ "name": "LIST",
+ "variableTypes": [Blockly.LIST_VARIABLE_TYPE],
+ },
+ {
+ "type": "input_value",
+ "name": "VALUE"
+ }
+ ],
+ "category": Blockly.Categories.dataLists,
+ "extensions": ["contextMenu_getListBlock", "colours_data_lists", "shape_statement"],
+ });
+ }
+ };
+
+ const oldDataCategory = Blockly.DataCategory;
+
+ // Patch DataCategory to add our own block
+ const patch = Blockly.DataCategory = (workspace: any) => {
+ let xmlList;
+ try {
+ Blockly.DataCategory = oldDataCategory;
+ xmlList = oldDataCategory(workspace);
+ }
+ finally {
+ Blockly.DataCategory = patch;
+ }
+
+ const firstListOperation = xmlList.findIndex((x: Element) => x.getAttribute('type') === 'data_addtolist');
+ const operationPatched = xmlList.findIndex((x: Element) => x.getAttribute('type') === 'data_setlistto') >= 0;
+ if ((firstListOperation >= 0) && (!operationPatched)) {
+ const variableModelList = workspace.getVariablesOfType(Blockly.LIST_VARIABLE_TYPE);
+ variableModelList.sort(Blockly.VariableModel.compareByName);
+ const firstVariable = variableModelList[0];
+
+ const setListTo = createDom('block', { type: 'data_setlistto', gap: '8' } );
+ const listVal = createDom('field', { name: 'LIST', variabletype: "list" });
+ listVal.innerText = firstVariable.name;
+ setListTo.appendChild(listVal);
+
+ const value = createDom('value', { name: 'VALUE' });
+ const shadow = createDom('shadow', {type: Blockly.LIST_VARIABLE_TYPE});
+ shadow.appendChild(createDom('field', { name: 'LIST' }));
+ value.appendChild(shadow);
+ setListTo.appendChild(value);
+
+ xmlList.splice(firstListOperation, 0, setListTo);
+ }
+
+ // Remove show/hide list and variable operations.
+ return xmlList.filter((op: Element) => {
+ const t = op.getAttribute('type');
+
+ const unused_operation = [
+ 'data_showvariable',
+ 'data_hidevariable',
+ 'data_showlist',
+ 'data_hidelist',
+ ].indexOf(t) >= 0;
+
+ return !unused_operation;
+ });
+ };
}
injectJSONBlocks() {
@@ -240,6 +612,224 @@ export class Toolbox {
};
}
+ injectOperationBlocks(connections: BridgeConnection[]) {
+ const bridge_to_connection_map: { [ key: string ]: BridgeConnection[] } = {};
+ const bridge_dropdown: [ string, string ][] = [];
+ const full_connection_dropdown: [ string, string ][] = [];
+
+ for (const conn of connections) {
+ if (bridge_to_connection_map[conn.bridge_id] === undefined) {
+ bridge_to_connection_map[conn.bridge_id] = [];
+ bridge_dropdown.push([conn.bridge_name, conn.bridge_id]);
+ }
+ bridge_to_connection_map[conn.bridge_id].push(conn);
+
+ full_connection_dropdown.push([conn.name || 'default', conn.connection_id]);
+ }
+
+ if (bridge_dropdown.length == 0) {
+ bridge_dropdown.push(["Unkown", '__plaza_internal_unlisted'])
+ }
+ Blockly.Blocks['trigger_on_bridge_connected'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'trigger_on_bridge_connected',
+ 'message0': 'When bridge %1 connects',
+ 'args0': [
+ {
+ 'type': 'field_dropdown',
+ 'name': 'BRIDGE1',
+ options: bridge_dropdown,
+ },
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_advanced_operation', 'shape_hat']
+ });
+ }
+ };
+
+ Blockly.Blocks['trigger_on_bridge_disconnected'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'trigger_on_bridge_disconnected',
+ 'message0': 'When bridge %1 connection STOPS',
+ 'args0': [
+ {
+ 'type': 'field_dropdown',
+ 'name': 'BRIDGE1',
+ options: bridge_dropdown,
+ },
+ ],
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_advanced_operation', 'shape_hat']
+ });
+ }
+ };
+
+ Blockly.Blocks['operator_select_connection'] = {
+ init: function () {
+
+ let currentBridgeSelected: string = null;
+ let connectionDropDown = bridge_dropdown;
+ if (connectionDropDown.length == 0) {
+ connectionDropDown = [[ 'No bridges found', '__not_found_error__' ]];
+ }
+
+ this.jsonInit({
+ 'id': 'operator_select_connection',
+ 'message0': 'On bridge %1 use connection %2',
+ 'args0': [
+ {
+ 'type': 'field_dropdown',
+ 'name': 'BRIDGE1',
+ 'options': connectionDropDown,
+ },
+ {
+ 'type': 'field_dropdown',
+ 'name': 'CONNECTION2',
+ 'options': () => {
+ if (!currentBridgeSelected) {
+ if (full_connection_dropdown.length > 0) {
+ return full_connection_dropdown;
+ }
+ else {
+ return [ [ "Bridge not found error", '__bridge_not_found_error' ] ];
+ }
+ }
+ try {
+ const options = [];
+
+ let bridge = currentBridgeSelected;
+ if (!currentBridgeSelected) {
+ bridge = bridge_dropdown[0][0];
+ }
+
+ for (const conn of bridge_to_connection_map[bridge]) {
+ options.push([conn.name || "default", conn.connection_id]);
+ }
+
+ return options;
+ }
+ catch (err) {
+ console.error(err);
+ return [ [ "Bridge not found error", '__bridge_not_found_error' ] ];
+ }
+ },
+ },
+ ],
+ message1: "%1",
+ args1: [
+ {
+ 'type': 'input_statement',
+ 'name': 'SUBSTACK3'
+ },
+ ],
+ lastDummyAlign2: "RIGHT",
+ message2: "when done, restore the previous connection",
+ args2: [],
+ 'category': Blockly.Categories.control,
+ 'previousStatement': null,
+ 'nextStatement': null,
+ 'extensions': ['colours_advanced_operation', 'shape_statement'],
+ });
+
+ const inputs = this.inputList[0].fieldRow;
+
+ let bridgeDropdown: { onHide: () => void; value_: string; } = null;
+ let connectionDropdown: { value_: any; getOptions: () => any; setValue: (arg0: any) => void; } = null;
+
+ for (const input of inputs) {
+ if (input.name === 'BRIDGE1') {
+ bridgeDropdown = input;
+ }
+ else if (input.name === 'CONNECTION2') {
+ connectionDropdown = input;
+ }
+ }
+
+ if (!bridgeDropdown) {
+ return;
+ }
+
+ // Capture the BridgeDropdown selector.
+ const hideBridgeDropdown = bridgeDropdown.onHide;
+ bridgeDropdown.onHide = () => {
+ hideBridgeDropdown.bind(bridgeDropdown)();
+ currentBridgeSelected = bridgeDropdown.value_;
+
+ // Refresh connection dropdown
+ const oldValue = connectionDropdown.value_;
+ const newOptions = connectionDropdown.getOptions();
+
+ // If oldValue in newOptions, just pick the first
+ let found = false;
+ for (const option of newOptions) {
+ if (option[1] === oldValue) {
+ found = true;
+ }
+ }
+
+ if ((!found) && (newOptions.length > 0)) {
+ connectionDropdown.setValue(newOptions[0][1]);
+ }
+ }
+
+ // HACK: Extract the current bridge selected *after* the initialization
+ // "thread" is completed. Otherwise doing this here would mean that
+ // the value would not be correct when blocks are loaded from XML.
+ setTimeout(() => {
+ currentBridgeSelected = bridgeDropdown.value_;
+ }, 0);
+ }
+ };
+
+ try {
+ Blockly.Extensions.register('colours_advanced_operation',
+ function () {
+ this.setColourFromRawValues_("#009688",
+ "#007668",
+ "#007668");
+ });
+ } catch (e) {
+ // If the extension was registered before
+ // this would have thrown an inocous exception
+ if (!alreadyRegisteredException(e)) {
+ throw e;
+ }
+ }
+
+ }
+
+ genOperationCategory(): HTMLElement {
+
+ const category = createDom('category', {
+ name: 'Advanced operation',
+ colour: "#009688",
+ secondaryColour: "#007668",
+ id: "operation",
+ });
+
+ category.appendChild(createDom('block',
+ {
+ type: "trigger_on_bridge_disconnected",
+ id: "trigger_on_bridge_disconnected",
+ }));
+
+ category.appendChild(createDom('block',
+ {
+ type: "trigger_on_bridge_connected",
+ id: "trigger_on_bridge_connected",
+ }));
+
+ category.appendChild(createDom('block',
+ {
+ type: "operator_select_connection",
+ id: "operator_select_connection",
+ }));
+
+ return category;
+ }
+
injectMonitorBlocks(monitors: MonitorMetadata[]) {
for (const monitor of monitors) {
Blockly.Blocks['monitor.retrieve.' + monitor.id] = {
@@ -272,6 +862,25 @@ export class Toolbox {
}
}
+ injectCustomControls() {
+ Blockly.Blocks['logging_add_log'] = {
+ init: function () {
+ this.jsonInit({
+ 'id': 'logging_add_log',
+ 'message0': 'Log %1',
+ 'args0': [
+ {
+ 'type': 'input_value',
+ 'name': 'STRING1'
+ }
+ ],
+ 'category': Blockly.Categories.control,
+ 'extensions': ['colours_control', 'shape_statement']
+ });
+ }
+ };
+ }
+
static get_bridge_color(bridge_id: string): string {
return '#' + bridge_id.replace(/\D/g,'').substring(0,6);
}
@@ -293,21 +902,72 @@ export class Toolbox {
return '#' + sec_color;
}
- injectCustomBlocks(categorized_custom_blocks: CategorizedCustomBlock[]) {
+ private static getConnectionForCategory(block_cat: CategorizedCustomBlock, connections: BridgeConnection[]): BridgeConnection {
+ for (const conn of connections) {
+ if (block_cat.bridge_data.bridge_id === conn.bridge_id) {
+ return conn;
+ }
+ }
+
+ return null;
+ }
+
+ private getIconForBlocks(block_cat: CategorizedCustomBlock, connections: BridgeConnection[]): string{
+ const conn = Toolbox.getConnectionForCategory(block_cat, connections);
+ if (!conn) {
+ return null;
+ }
+
+ return iconDataToUrl(this.environmentService, conn.icon, block_cat.bridge_data.bridge_id);
+ }
+
+ injectCustomBlocks(categorized_custom_blocks: CategorizedCustomBlock[], connections: BridgeConnection[]) {
for (const blocks of categorized_custom_blocks){
for (const block of blocks.resolved_custom_blocks) {
+ const toolbox = this;
Blockly.Blocks[block.id] = {
- init: function () {
- this.jsonInit({
- 'id': block.id,
- 'message0': block.message,
- 'category': Blockly.Categories.event,
- 'extensions': ['colours_bridge_' + blocks.bridge_data.bridge_id,
- get_block_category(block)],
- 'args0': get_block_toolbox_arguments(block)
- });
- }
- };
+ init: function () {
+ const block_args = get_block_toolbox_arguments(block);
+ const icon = toolbox.getIconForBlocks(blocks, connections);
+ let msg_prefix = `%${block_args.length + 1} `;
+ let label_arg: ScratchBlockArgument = {
+ type: 'field_label',
+ // The space after the label is duplicated (with the msg_prefix) for more clear separation
+ text: `[${blocks.bridge_data.bridge_name}] `,
+ };
+
+ if (icon) {
+ label_arg = {
+ type: 'field_image',
+ src: icon,
+ width: 48,
+ height: 24,
+ alt: blocks.bridge_data.bridge_name,
+ flip_rtl: false,
+ };
+ }
+
+ let message = block.message;
+ // HACK: avoid ending with a field (%1 for a dropdown) or
+ // Blockly.Block.interpolate_ will not flush the
+ // `fieldStack`.
+ if (message.match(/%[0-9]+$/)) {
+ // The character added is a "Braille Pattern Blank", which
+ // looks like a space but won't get trimmed away.
+ // TODO: Fix Blockly.Block.interpolate_ to remove the need for this.
+ message += '\u2800';
+ }
+
+ this.jsonInit({
+ 'id': block.id,
+ 'message0': msg_prefix + message,
+ 'category': Blockly.Categories.event,
+ 'extensions': ['colours_bridge_' + blocks.bridge_data.bridge_id,
+ get_block_category(block)],
+ 'args0': block_args.concat([ label_arg ])
+ });
+ }
+ };
}
if (blocks.resolved_custom_blocks.length > 0) {
@@ -338,7 +998,11 @@ export class Toolbox {
continue;
}
- const custom_blocks_xml = custom_blocks.resolved_custom_blocks.map(
+ const custom_blocks_xml = custom_blocks.resolved_custom_blocks.filter(
+ (block) => {
+ return block.show_in_toolbox !== false;
+ }
+ ).map(
(block, _index, _array) => {
return block_to_xml(block);
});
@@ -397,6 +1061,13 @@ export class Toolbox {
-->
+
+
+
+ Hello!
+
+
+
@@ -570,6 +1242,7 @@ export class Toolbox {
+
-
-
-
-
-
-
diff --git a/frontend/src/app/dashboard.component.ts b/frontend/src/app/dashboard.component.ts
deleted file mode 100644
index 1013502b..00000000
--- a/frontend/src/app/dashboard.component.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import * as progbar from './ui/progbar';
-
-import { HowToEnableServiceDialogComponent } from './HowToEnableServiceDialogComponent';
-
-import { Component, OnInit } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { ProgramMetadata } from './program';
-import { ProgramService } from './program.service';
-
-import { Session } from './session';
-import { SessionService } from './session.service';
-
-import { AvailableService, ServiceEnableHowTo } from './service';
-import { ServiceService } from './service.service';
-import { MatDialog } from '@angular/material/dialog';
-import { MatSlideToggle } from '@angular/material/slide-toggle';
-import { MatSlideToggleChange } from '@angular/material/slide-toggle';
-
-import { MonitorMetadata } from './monitor';
-import { MonitorService } from './monitor.service';
-import { BridgeService } from './bridges/bridge.service';
-import { BridgeIndexData } from './bridges/bridge';
-import { BridgeDeleteDialogComponent } from './bridges/delete-dialog.component';
-
-@Component({
- // moduleId: module.id,
- selector: 'app-my-dashboard',
- templateUrl: './dashboard.component.html',
- providers: [BridgeService, MonitorService, ProgramService, SessionService, ServiceService],
- styleUrls: [
- 'dashboard.component.css',
- 'libs/css/material-icons.css',
- 'libs/css/bootstrap.min.css',
- ],
-})
-
-export class DashboardComponent {
- programs: ProgramMetadata[] = [];
- services: AvailableService[] = [];
- monitors: MonitorMetadata[] = [];
- session: Session = null;
-
- constructor(
- private programService: ProgramService,
- private serviceService: ServiceService,
- private monitorService: MonitorService,
- private sessionService: SessionService,
- private router: Router,
- public dialog: MatDialog,
- ) {
- this.programService = programService;
- this.serviceService = serviceService;
- this.monitorService = monitorService;
- this.sessionService = sessionService;
- this.router = router;
- }
-
- // tslint:disable-next-line:use-life-cycle-interface
- ngOnInit(): void {
- this.sessionService.getSession()
- .then(session => {
- this.session = session;
- if (!session.active) {
- this.router.navigate(['/login']);
- } else {
- this.programService.getPrograms()
- .then(programs => this.programs = programs);
-
- this.serviceService.getAvailableServices()
- .then(services => this.services = services);
-
- this.monitorService.getMonitors()
- .then(monitors => this.monitors = monitors);
- }
- })
- .catch(e => {
- console.log('Error getting session', e);
- this.router.navigate(['/login']);
- })
- }
-
- addProgram(): void {
- this.programService.createProgram().then(program => {
- this.openProgram(program);
- });
- }
-
- openProgram(program: ProgramMetadata): void {
- this.sessionService.getSession().then(session =>
- this.router.navigate(['/users/' + session.username
- + '/programs/' + encodeURIComponent(program.name)]));
- }
-
- enableService(service: AvailableService): void {
- this.serviceService.getHowToEnable(service)
- .then(howToEnable => this.showHowToEnable(howToEnable));
- }
-
- showHowToEnable(howTo: ServiceEnableHowTo): void {
- if ((howTo as any).success === false) {
- return;
- }
- const dialogRef = this.dialog.open(HowToEnableServiceDialogComponent, {
- data: howTo
- });
-
- dialogRef.afterClosed().subscribe(result => { });
- }
-
- onChange(ob: MatSlideToggleChange, program: ProgramMetadata) {
- program.enabled = ob.checked;
- console.log(ob.checked);
- this.sessionService.getSession().then(session =>
- this.programService.setProgramStatus(JSON.stringify({"enable":ob.checked}), program.id, session.user_id));
- let matSlideToggle: MatSlideToggle = ob.source;
- }
-}
diff --git a/frontend/src/app/dashboard/dashboard.component.css b/frontend/src/app/dashboard/dashboard.component.css
new file mode 100644
index 00000000..e4129bf0
--- /dev/null
+++ b/frontend/src/app/dashboard/dashboard.component.css
@@ -0,0 +1,611 @@
+.profile-section {
+ width: 100%;
+ --long-animation-time: 0.3s;
+}
+
+div.letter-call-to-action {
+ background-color: darkorange;
+ font-size: small;
+ padding: 1ex;
+ border-radius: 2px;
+ margin-top: 1ex;
+}
+
+mat-card.module > h4 {
+ overflow: hidden;
+ font-size: 1.15rem;
+ cursor: pointer;
+}
+
+/* Program styling */
+.row.program-list {
+ margin-right: 1ex;
+ margin-left: 1ex;
+}
+
+.row.program-list > .item {
+ padding: 0;
+}
+
+mat-card.program {
+ padding: 0;
+ margin: 1ex;
+}
+
+.program-data {
+ display: grid;
+ grid-template-rows: auto;
+ grid-template-columns: max-content 1fr;
+}
+
+.program-data > .program-type {
+ grid-row: 1;
+ grid-column: 1;
+ width: max-content;
+}
+
+.program-data > .program-type > img {
+ padding: 1ex;
+ max-width: 8ex;
+ max-height: 8ex;
+}
+
+mat-card.program > .program-data > .connection-icon-list {
+ background-color: rgba(0, 48, 150,0.05);
+ border-top: 1px solid rgba(0, 48, 150,0.25);
+
+ text-align: left;
+ padding: 1ex;
+
+ grid-row: 2;
+
+ grid-column-start: 1;
+ grid-column-end: 3;
+}
+
+mat-card.program > .program-data > .card-title {
+ grid-row: 1;
+ grid-column: 2;
+ word-break: break-word;
+
+ padding: 1ex;
+ font-weight: 600;
+}
+
+mat-card.call-to-action > .program-data > .card-title {
+ color: #fff;
+ padding: 2em 1em 2em 1em;
+
+ grid-row: 1;
+ grid-column: 2;
+}
+
+mat-card.program > .program-operation {
+ float: right;
+ margin-top: -2.5ex;
+ margin-bottom: 1ex;
+ margin-right: 0ex;
+}
+
+mat-card.program > .program-operation > .fab-action {
+ border-radius: 999ex; /* Inifinity, round shape */
+ color: white;
+ z-index: 1;
+}
+
+mat-card.program .program-settings {
+ position: absolute;
+ top: 0;
+ background: #27212e;
+ height: 100%;
+ width: 100%;
+ border-radius: 4px;
+ width: 0;
+}
+
+mat-card.program .program-settings .contents {
+ opacity: 0;
+ height: 100%;
+ width: 100%;
+ border-radius: 4px;
+ cursor: auto;
+}
+
+@keyframes hide-program-settings {
+ from { width: 100%; }
+ 25% { width: 100%; }
+ to { width: 0%; }
+}
+mat-card.program .program-settings.hidden-true {
+ width: 0%;
+ animation-name: hide-program-settings;
+ animation-duration: var(--long-animation-time);
+}
+
+@keyframes hide-program-settings-data {
+ from { opacity: 1; }
+ 25% { opacity: 0; }
+}
+mat-card.program .program-settings.hidden-true .contents {
+ opacity: 0;
+ animation-name: hide-program-settings-data;
+ animation-duration: var(--long-animation-time);
+}
+
+@keyframes show-program-settings {
+ from { left: 100%; width: 0%; }
+ 75% { left: 0%; width: 100%; }
+}
+mat-card.program .program-settings.hidden-false {
+ width: 100%;
+ animation-name: show-program-settings;
+ animation-duration: var(--long-animation-time);
+}
+
+@keyframes show-program-settings-data {
+ from { opacity: 0; }
+ 80% { opacity: 0; }
+ to { opacity: 1; }
+}
+mat-card.program .program-settings.hidden-false .contents {
+ opacity: 1;
+ animation-name: show-program-settings-data;
+ animation-duration: var(--long-animation-time);
+}
+
+mat-card.program .program-settings .contents .title {
+ color: white;
+ font-weight: bold;
+ margin-top: 0.5ex;
+}
+
+mat-card.program .program-settings .contents .explanation {
+ color: white;
+ margin-bottom: 1ex;
+}
+
+mat-card.program .program-settings .contents .title .program-name {
+ color: #ffab40;
+}
+
+mat-card.program .program-settings .contents button.archive-program {
+ background-color: #009688;
+ color: white;
+ padding: 1ex;
+ border-radius: 4px;
+}
+
+/* Public section */
+.public-section {
+ width: 100%;
+ padding-top: 1em;
+}
+
+.col > h1 {
+ text-align: center;
+}
+
+.section-explanation {
+ padding: 1ex;
+ background-color: rgba(0,0,0,0.1);
+ margin-bottom: 1ex;
+ border-radius: 5px;
+ font-weight: bold;
+}
+
+.public-section > .user-programs {
+ /* margin: 0 auto; */
+ padding-bottom: 3em;
+}
+
+
+/* Private section. Mostly, connections upper "bar" */
+.private-section {
+ background: repeating-linear-gradient(45deg,#fafafa,#fafafa 10px,#f3f3f3 10px, #f3f3f3 20px);
+ box-shadow: 0px 0px 2px 1px rgba(0,0,0,0.3);
+ overflow-x: auto;
+ padding: 1em 0 1em 0;
+}
+
+.user-connections {
+ margin: 0 auto;
+ width: max-content;
+}
+
+.user-connections > h1 {
+ display: block;
+ font-size: 150%;
+ width: 100%;
+ max-width: 100vw;
+ text-align: center;
+}
+
+.user-connections > .connection-list {
+ padding: 0 1em 0 1em;
+}
+
+.user-connections > .connection-list > div {
+ display: inline;
+ margin-right: 1ex;
+}
+
+.user-connections > .connection-list > div > mat-card {
+ display: inline-block;
+ padding: 1ex;
+}
+
+.user-connections > .connection-list > div > mat-card > mat-icon {
+ vertical-align: bottom;
+}
+
+.user-connections > .connection-list > div > mat-card > .connection-name {
+ display: inline-block;
+ min-height: 3ex;
+}
+
+.user-connections > .connection-list > div > mat-card.shared-resource {
+ padding: 0;
+}
+.user-connections .shared-resource .connection-type {
+ height: 5ex;
+ display: inline-block;
+ vertical-align: middle;
+ padding: 0 1ex 0 1ex;
+
+ background-color: #27212e;
+ color: #fff;
+ border-radius: 3px 0 0 3px;
+}
+
+.user-connections .shared-resource .connection-type mat-icon {
+ margin-top: 0.75ex;
+}
+
+.user-connections .shared-resource .connection-identifier {
+ padding: 0 1ex 0 1ex;
+ display: inline-block;
+}
+
+.user-connections .shared-resource img {
+ height: 3ex;
+ max-width: 10ex;
+}
+
+.connection > img.icon {
+ height: 3ex;
+ max-width: 10ex;
+}
+
+/* Call-to-actions */
+.user-connections .integrated-call-to-action {
+ display: inline-block;
+ font-size: 200%;
+ color: #ff4500;
+ font-weight: bold;
+}
+
+/* Examples & Tutorials */
+mat-card.example {
+ background-color: #3798e2;
+ color: #fff;
+ font-weight: 600;
+
+ padding: 0; /* Moved into child elements */
+}
+
+mat-card.example > .description {
+ padding: 1ex;
+}
+
+.connection-icon-list {
+ border-radius: 0 0 4px 4px;
+ background-color: rgba(255,255,255,0.95);
+ text-align: left;
+ padding: 1ex;
+ min-height: 5ex;
+}
+
+.connection-icon-list > img {
+ height: 3ex;
+ max-width: 10ex;
+ margin-left: 1ex;
+}
+
+.connection-icon-list > span > img {
+ height: 3ex;
+ max-width: 10ex;
+ margin-left: 1ex;
+}
+
+
+.connection-icon-list > span > .nametag {
+ margin-left: 1ex;
+ padding: 0 1ex 0 1ex;
+ display: inline-block;
+ background-color: #fff;
+ box-shadow: 0px 1px 1px 0px rgba(0,0,0,0.3);
+ border-radius: 3px;
+ min-height: 3ex;
+}
+
+/* User profile */
+.profile {
+ margin: 1em auto;
+ max-width: 100%;
+}
+
+.profile .avatar {
+ text-align: center;
+}
+
+.profile .avatar img {
+ height: 10em;
+ width: 10em;
+ border-radius: 4px;
+}
+
+.profile .profile-name {
+ font-size: 200%;
+ color: #444;
+ text-align: center;
+}
+
+.status-ispublic-info {
+ text-align: center;
+ font-style: italic;
+}
+
+.status-ispublic-info mat-icon {
+ vertical-align: bottom;
+}
+
+.profile .section-title {
+ font-size: 150%;
+}
+
+.profile .no-group-joined-explanation {
+ font-style: italic;
+ display: inline;
+ padding-left: 1em;
+}
+
+.keyword {
+ text-decoration: underline;
+}
+
+mat-card.group, mat-card.collaborator {
+ display: inline-block;
+ vertical-align: bottom;
+ margin-left: 1ex;
+ margin-top: 1ex;
+ padding: 0;
+ height: 3em;
+ min-width: 3em;
+ text-align: center;
+}
+
+mat-card.group img {
+ max-width: 3em;
+ height: 3em;
+ border-radius: 4px;
+}
+
+mat-card.group .group-name {
+ display: block;
+ padding: 1ex;
+ margin-top: 0.5ex;
+}
+
+section .section-buttons {
+ margin-top: 1ex;
+}
+
+button.group {
+ padding: 1ex;
+ margin-left: 1ex;
+ margin-top: 1ex;
+}
+
+button.cardlike {
+ height: 5ex;
+ min-width: auto;
+ padding: 1ex;
+ border-radius: 4px;
+ display: inline;
+}
+
+
+.profile section {
+ margin: 1em auto;
+ padding-top: 1ex;
+ width: max-content;
+ max-width: 100%;
+ border-top: 1px solid #aaa;
+}
+
+section.bridges .not-joined-explanation {
+ margin: 1em;
+}
+
+section.bridges {
+ border-top: 1px solid rgba(0,0,0,0.3);
+}
+
+section.bridges .row {
+ margin: 0;
+}
+
+section.bridges .row .item-holder {
+ padding: 0;
+}
+
+.bridge.call-to-action .card-title {
+ height: 100%;
+ justify-content: center;
+ display: flex;
+ flex-direction: column;
+}
+
+.bridge.call-to-action .card-title mat-icon {
+ vertical-align: bottom;
+}
+
+section.bridges .bridges-maintained-subsection {
+ border-top: 1px solid #aaa;
+}
+
+section.bridges h4 {
+ margin: 1ex;
+}
+
+mat-card.bridge {
+ display: block;
+ vertical-align: bottom;
+ margin: 0.5ex 1ex 0.5ex 1ex;
+ margin-top: 1ex;
+ padding: 0;
+ height: 3em;
+
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+
+ -webkit-user-drag: none;
+ -webkit-tap-highlight-color: transparent;
+}
+
+mat-card.bridge .bridge-data {
+ display: grid;
+ overflow: hidden;
+ height: 100%;
+ grid-template-columns: max-content auto;
+}
+
+mat-card.bridge .bridge-name {
+ grid-row: 1;
+ grid-column: 2;
+ margin: auto 0;
+ padding: 0 0.5ex 0 0.5ex;
+ text-align: center;
+}
+
+mat-card.bridge img {
+ width: 100%;
+ max-height: 3em;
+ padding: 0.5ex;
+ margin: auto 0;
+
+ grid-row: 1;
+ grid-column: 2;
+ text-align: center;
+}
+
+mat-card.bridge .bridge-status {
+ height: 3em;
+ display: inline-block;
+ vertical-align: inherit;
+ padding-left: 1ex;
+ padding-right: 1ex;
+ grid-row: 1;
+ grid-column: 1;
+ width: max-content;
+
+ color: #fff;
+ border-radius: 3px 0 0 3px;
+}
+
+mat-card.bridge .bridge-status.connected-true {
+ background-color: #279f27;
+}
+mat-card.bridge .bridge-status.connected-false {
+ background-color: #9f2727;
+}
+
+mat-card.bridge .bridge-status mat-icon {
+ padding-top: 1ex;
+}
+
+mat-card.non-clickable {
+ box-shadow: none;
+ border: 1px solid rgba(0,0,0,0.3);
+ cursor: inherit;
+}
+
+.edit-configuration {
+ text-align: center;
+ margin-top: 1em;
+}
+
+.edit-configuration .settings-link {
+ background-color: #fc8;
+ color: #000;
+ padding: 1ex;
+ box-shadow: 0px 2px 3px 0px rgba(0,0,0,0.3);
+ border-radius: 4px;
+ display: inline-block;
+}
+
+.edit-configuration .settings-link mat-icon {
+ vertical-align: bottom;
+}
+
+.edit-configuration .settings-link:hover {
+ text-decoration: none;
+}
+
+button.collaborators.call-to-action, button.group.call-to-action {
+ font-size: 75%;
+ vertical-align: bottom;
+ height: 2em;
+ font-weight: initial;
+ padding: 0.5ex 1ex;
+}
+
+mat-card.collaborator {
+ display: inline-block;
+ vertical-align: bottom;
+ margin-left: 1ex;
+ margin-top: 1ex;
+ padding: 0;
+ box-sizing: content-box;
+}
+
+.collaborator .collaborator-role {
+ height: 3em;
+ display: inline-block;
+ vertical-align: inherit;
+ padding-left: 1ex;
+ padding-right: 1ex;
+
+ background-color: #27212e;
+ color: #fff;
+ border-radius: 3px 0 0 3px;
+}
+
+mat-card.collaborator .collaborator-role mat-icon {
+ vertical-align: middle;
+ font-size: 140%;
+ margin-top: 1ex;
+}
+
+mat-card.collaborator img {
+ max-width: 10ex;
+ width: 3em;
+ text-align: center;
+ height: 3em;
+ border-radius: 0 4px 4px 0;
+ display: inline-block;
+}
+
+mat-card.collaborator .user-name {
+ display: inline-block;
+ padding: 1ex;
+ height: 3em;
+}
+
+mat-card.disabled {
+ background: repeating-linear-gradient(45deg, #fff, #fff 10px, #eee 10px, #eee 20px);
+ color: #222;
+ pointer-events: none;
+}
diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html
new file mode 100644
index 00000000..bb4b1782
--- /dev/null
+++ b/frontend/src/app/dashboard/dashboard.component.html
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+ Programs
+
+
+
+ Create a new program or edit the ones you already have.
+
+
+
+
+
+
+
+
+
+
{{program.name}}
+
+
+
+
+ {{ bridgeInfo[bridgeId].name }}
+
+
+
+
+
+
+
Archive {{ program.name }} ?
+
The program will stop until you re-start it.
+
+ archive
+ Archive
+
+
+
+
+
+
+ archive
+
+
+
+
+
+
+
+
+
+ Archived programs
+
+
+
+ These are programs that you have archived, they won't be running until you launch them again.
+
+
+
+
+
+
+
+
{{program.name}}
+
+
+
+
+ {{ bridgeInfo[bridgeId].name }}
+
+
+
+
+
+
+ play_arrow
+
+
+
+
+
+
+
+
+
+ Bridges
+
+
+
+
+
{{ connections.length }} Connections to bridges
+
+ {{ profile.type === 'user' ? 'You haven\'t' : 'This group hasn\'t' }} established any connection to any bridge.
+
+
+
+
+
+
+ add
+ Connect to bridge
+
+
+
+
+
+
+
+
+
+ link
+ link_off
+
+
+
+
+
+ {{ connection.conn.bridge_name }}
+
+
+
+
+
+
+
+
+
{{ bridges.length }} Bridges
+
+ {{ profile.type === 'user' ? 'You don\'t' : 'This group doesn\'t' }} maintain any bridge.
+
+
+
+
+
+
+ add
+ Add new bridge
+
+
+
+
+
+
+
+
+ link
+ link_off
+
+
+
+
+
+ {{bridge.name || "unnamed bridge"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ profile.name }}
+
+
+
+
+
+ Groups
+
+
+ group_add New
+
+
+
+
+ You have not joined any group .
+
+
+
+
+
+
+
+ {{ group.name }}
+
+
+
+
+
+
+
+
+ Collaborators
+
+
+ edit Edit
+
+
+
+
+
+ This group has no public collaborator.
+
+
+
+
+
+ {{ _roleToIcon(user.role) }}
+
+
+
+
+
+ {{ user.username }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts
new file mode 100644
index 00000000..22d88d47
--- /dev/null
+++ b/frontend/src/app/dashboard/dashboard.component.ts
@@ -0,0 +1,492 @@
+import { Component, ViewChild } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { MatTabGroup } from '@angular/material/tabs';
+import { ActivatedRoute, Router } from '@angular/router';
+import { BridgeIndexData, SharedResource } from 'app/bridges/bridge';
+import { BrowserService } from 'app/browser.service';
+import { AddBridgeDialogComponent } from 'app/dialogs/add-bridge-dialog/add-bridge-dialog.component';
+import { EditCollaboratorsDialogComponent } from 'app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component';
+import { UpdateBridgeDialogComponent } from 'app/dialogs/update-bridge-dialog/update-bridge-dialog.component';
+import { GroupInfo } from 'app/group';
+import { GroupService } from 'app/group.service';
+import { Collaborator, CollaboratorRole, roleToIcon } from 'app/types/collaborator';
+import { BridgeService } from '../bridges/bridge.service';
+import { MonitorService } from '../monitor.service';
+import { ProgramMetadata, ProgramType } from '../program';
+import { ProgramService } from '../program.service';
+import { SelectProgrammingModelDialogComponent } from '../programs/select-programming-model-dialog/select-programming-model-dialog.component';
+import { ServiceService } from '../service.service';
+import { Session } from '../session';
+import { SessionService } from '../session.service';
+import { getGroupPictureUrl, getUserPictureUrl, iconDataToUrl } from '../utils';
+import { ConnectionService } from 'app/connection.service';
+import { BridgeConnectionWithIconUrl, IconReference } from 'app/connection';
+import { EnvironmentService } from 'app/environment.service';
+import { ProfileService, GroupProfileInfo } from 'app/profiles/profile.service';
+import { Subscription } from 'rxjs';
+import { AddConnectionDialogComponent } from 'app/connections/add-connection-dialog.component';
+import { ConnectToAvailableDialogComponent } from 'app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component';
+
+type TutorialData = { description: string, icons: string[], url: string };
+
+@Component({
+ // moduleId: module.id,
+ selector: 'app-my-dashboard',
+ templateUrl: './dashboard.component.html',
+ providers: [BridgeService, ConnectionService, GroupService, MonitorService, ProgramService, SessionService, ServiceService],
+ styleUrls: [
+ 'dashboard.component.css',
+ '../libs/css/material-icons.css',
+ '../libs/css/bootstrap.min.css',
+ ],
+})
+export class DashboardComponent {
+ programs: ProgramMetadata[] = [];
+ connections: BridgeConnectionWithIconUrl[] = null;
+
+ session: Session = null;
+ profile: {type: 'user' | 'group', name: string, groups: GroupInfo[], picture: string};
+ bridgeInfo: { [key:string]: { icon: string, name: string }} = {};
+ bridgesById: { [key:string]: BridgeIndexData} = {};
+ collaborators: Collaborator[] = null;
+
+ bridges: BridgeIndexData[] = null;
+ tutorials: TutorialData[] = [
+ {
+ description: "Create a weather chatbot",
+ icons: [ "/assets/icons/telegram_logo.png", "/assets/icons/aemet_logo.png" ],
+ url: "https://docs.programaker.com/tutorials/weather-bot.html",
+ },
+ ];
+ programSettingsOpened: { [key: string]: false | 'archive' } = {};
+
+ sharedResources: SharedResource[];
+
+ @ViewChild('navTabGroup') navTabGroup: MatTabGroup;
+
+ tabFragName = [
+ 'profile',
+ 'programs',
+ 'archived-programs',
+ 'bridges',
+ 'info',
+ ];
+ groupInfo: GroupInfo;
+ userRole: CollaboratorRole | null;
+ groupProfile: GroupProfileInfo;
+
+ canWriteToGroup: boolean;
+ bridgesQuery: Promise;
+
+ readonly _getUserPicture: (userId: string) => string;
+ readonly _iconDataToUrl: (icon: IconReference, bridge_id: string) => string;
+ isOwnUser: boolean;
+ private _isReadyForLoadingTabs: boolean = true;
+ private _moveToTab: () => void;
+
+ constructor(
+ private browser: BrowserService,
+ private programService: ProgramService,
+ private sessionService: SessionService,
+ private groupService: GroupService,
+ private connectionService: ConnectionService,
+ private router: Router,
+ private route: ActivatedRoute,
+ private environmentService: EnvironmentService,
+ private profileService: ProfileService,
+
+ private dialog: MatDialog,
+ private bridgeService: BridgeService,
+ ) {
+ this._getUserPicture = getUserPictureUrl.bind(this, environmentService);
+ this._iconDataToUrl = iconDataToUrl.bind(this, environmentService);
+
+ this.route.data
+ .subscribe((data: { programs: ProgramMetadata[] }) => {
+ this.programs = data.programs?.sort((a, b) => {
+ return a.name.localeCompare(b.name, undefined, { ignorePunctuation: true, sensitivity: 'base' });
+ });
+ });
+ }
+
+ ngOnInit(): void {
+ this.sessionService.getSession()
+ .then(async (session) => {
+ this.session = session;
+ const params = (this.route.params as any)['value'];
+ if (params.group_name !== undefined) {
+ this.isOwnUser = false;
+
+ // Group Dashboard
+ const groupName = params.group_name;
+
+ this.profile = {
+ name: groupName,
+ 'type': 'group',
+ groups: null,
+ picture: null,
+ };
+
+ this.groupProfile = await this.profileService.getProfileFromGroupname(groupName);
+
+ this.groupService.getGroupWithName(groupName).then(groupInfo => {
+ this.groupInfo = groupInfo;
+ this.profile.picture = getGroupPictureUrl(this.environmentService, this.groupInfo.id);
+
+ this.bridgesQuery = this.updateBridges();
+ this.updateConnections();
+
+ this.updateSharedResources();
+ return this.updateCollaborators();
+ })
+ .catch(err => console.error(err))
+ .then(() => this._tabReady())
+ }
+ else {
+ if (!session.active) {
+ this.router.navigate(['/login'], {replaceUrl:true});
+ } else {
+ this.isOwnUser = true;
+ this._tabReady();
+
+ this.profile = {
+ name: session.username,
+ 'type': 'user',
+ groups: null,
+ picture: getUserPictureUrl(this.environmentService, session.user_id)
+ };
+
+ this.groupService.getUserGroups()
+ .then(groups => this.profile.groups = groups);
+ }
+
+ this.bridgesQuery = this.updateBridges();
+ this.updateConnections();
+ }
+ })
+ .catch(e => {
+ console.log('Error getting session', e);
+ this.router.navigate(['/login'], {replaceUrl:true});
+ });
+ }
+
+ _tabReady() {
+ // This activates `_moveToTab` when all the data necessary to know which
+ // tabs are to be shown is available. In case `_moveToTab` is not
+ // available yet, it records that the necessary state has been reached
+ // so the function that defines `_moveToTab` can call it directly.
+
+ this._isReadyForLoadingTabs = true;
+ if (this._moveToTab) {
+ this._moveToTab();
+ }
+ }
+
+ ngAfterViewInit() {
+ let unsubscribe = false;
+ let subscription: Subscription = null;
+ // The same behavior might be achieved with .toPromise(), but it
+ // seems to have problems (with race conditions?).
+ subscription = this.route.fragment.subscribe({
+ next: (fragment => {
+ this._moveToTab = (() => {
+ const idx = this.tabFragName.indexOf(fragment) + (this._isProfileTabPresent() ? 0 : - 1);
+ if (idx >= 0) {
+ this.navTabGroup.selectedIndex = idx;
+ }
+
+ if (subscription !== null) {
+ subscription.unsubscribe();
+ }
+ else {
+ // In case the subscription assignation has not happened yet, take note of it to
+ // unsubscribe as soon as possible.
+ unsubscribe = true;
+ }
+ });
+ if (this._isReadyForLoadingTabs) {
+ this._moveToTab();
+ }
+ })
+ });
+ if (unsubscribe) {
+ // The first value has read before the `subcription` variable has been assigned.
+ // Now the only thing that remains is to perform the unsubscription.
+ subscription.unsubscribe();
+ }
+
+ this.navTabGroup.selectedIndexChange.subscribe({
+ next: (idx: number) => {
+ const currState = history.state;
+
+ history.replaceState(currState, '', this.updateAnchor(this.browser.window.location.href, this.getTabFragName(idx)));
+ this.programSettingsOpened = {};
+ }
+ });
+ }
+
+ private getTabFragName(idx: number) {
+ return this.tabFragName[this._isProfileTabPresent() ? idx : idx + 1];
+ }
+
+ _isProfileTabPresent() {
+ return this.profile && this.profile.type === 'group' && this.groupProfile;
+ }
+
+ private updateAnchor(href: string, anchor: string): string {
+ const anchorStart = href.indexOf('#');
+ if (anchorStart < 0) {
+ return href + '#' + anchor;
+ }
+ else {
+ return href.substring(0, anchorStart) + '#' + anchor;
+ }
+ }
+
+ addProgram(): void {
+ const dialogRef = this.dialog.open(SelectProgrammingModelDialogComponent, { width: '90%', data: {
+ is_advanced_user: this.session.tags.is_advanced,
+ is_user_in_preview: this.session.tags.is_in_preview,
+ }});
+
+ dialogRef.afterClosed().subscribe((result: {success: boolean, program_type: ProgramType, program_name: string}) => {
+ if (result && result.success) {
+ let programCreation: Promise;
+ if (this.groupInfo) {
+ programCreation = this.programService.createProgramOnGroup(result.program_type, result.program_name, this.groupInfo.id);
+ }
+ else {
+ programCreation = this.programService.createProgram(result.program_type, result.program_name);
+ }
+
+ programCreation.then(program => this.openProgram(program));
+ }
+ });
+ }
+
+ addConnection(): void {
+ const dialogRef = this.dialog.open(ConnectToAvailableDialogComponent, { width: '90%',
+ data: { groupId: this.groupInfo?.id }});
+
+ dialogRef.afterClosed().subscribe((result: {success: boolean}) => {
+ if (result && result.success) {
+ this.updateConnections();
+ }
+ });
+ }
+
+ addBridge(): void {
+ const dialogRef = this.dialog.open(AddBridgeDialogComponent, { width: '80%',
+ data: { groupId: this.groupInfo?.id },
+ });
+
+ dialogRef.afterClosed().subscribe((result: {success: boolean, bridgeId?: string, bridgeName?: string}) => {
+ if (result && result.success) {
+ this.updateBridges();
+
+ this.openBridgePanel({ id: result.bridgeId, name: result.bridgeName });
+ }
+ });
+ }
+
+ async updateBridges() {
+ if (this.groupInfo) {
+ this.bridges = await this.bridgeService.listGroupBridges(this.groupInfo.id);
+ }
+ else {
+ this.bridges = (await this.bridgeService.listUserBridges()).bridges;
+ }
+ this.bridges.sort((a, b) => {
+ return a.name.localeCompare(b.name, undefined, { ignorePunctuation: true, sensitivity: 'base' });
+ });
+
+ for (const bridge of this.bridges) {
+ this.bridgeInfo[bridge.id] = {
+ name: bridge.name,
+ icon: iconDataToUrl(this.environmentService, bridge.icon, bridge.id)
+ };
+ this.bridgesById[bridge.id] = bridge;
+ }
+ }
+
+
+ async updateSharedResources() {
+ if (!this.groupInfo) {
+ return;
+ }
+
+ this.sharedResources = await this.groupService.getSharedResources(this.groupInfo.id);
+
+ for (const conn of this.sharedResources){
+ this.bridgeInfo[conn.bridge_id] = {
+ icon: iconDataToUrl(this.environmentService, conn.icon, conn.bridge_id),
+ name: conn.name
+ };
+ }
+ }
+
+ addCollaborators(): void {
+ const dialogRef = this.dialog.open(EditCollaboratorsDialogComponent, { width: '90%', maxHeight: '100vh', maxWidth: '100vw',
+ data: { groupId: this.groupInfo.id,
+ existingCollaborators: this.collaborators,
+ },
+ });
+
+ dialogRef.afterClosed().subscribe(async (result: {success: boolean}) => {
+ if (result && result.success) {
+ this.updateCollaborators();
+ }
+ });
+ }
+
+ async updateCollaborators() {
+ if (!this.groupInfo) {
+ return;
+ }
+
+ const collaborators = await this.groupService.getCollaboratorsOnGroup(this.groupInfo.id)
+
+ collaborators.sort((a, b) => {
+ // First try to sort by role
+ if ((a.role === 'admin' && b.role !== 'admin') ||
+ (a.role === 'editor' && b.role === 'viewer')) {
+ return -1;
+ }
+
+ if ((b.role === 'admin' && a.role !== 'admin') ||
+ (b.role === 'editor' && a.role === 'viewer')) {
+ return 1;
+ }
+
+ // Else, sort alphabetically by username
+ return a.username.localeCompare(b.username, undefined, { ignorePunctuation: true, sensitivity: 'base' });
+ });
+ this.collaborators = collaborators;
+
+ // Discover own user role
+ for (let user of collaborators) {
+ if (user.id == this.session.user_id) {
+ if ((!this.userRole) || (user.role === 'admin')
+ || (user.role === 'editor' && this.userRole !== 'admin')) {
+
+ this.userRole = user.role;
+ }
+ }
+ }
+ this.canWriteToGroup = (this.userRole === 'admin') || (this.userRole === 'editor');
+ }
+
+ async updateConnections() {
+ let connectionQuery;
+ if (this.groupInfo) {
+ connectionQuery = this.connectionService.getConnectionsOnGroup(this.groupInfo.id);
+ }
+ else {
+ connectionQuery = this.connectionService.getConnections();
+ }
+
+ const connections = await connectionQuery;
+ this.connections = connections.map((v, _i, _a) => {
+ const icon_url = iconDataToUrl(this.environmentService, v.icon, v.bridge_id);
+
+ return { conn: v, extra: {icon_url: icon_url }};
+ }).sort((a, b) => {
+ return a.conn.bridge_name.localeCompare(b.conn.bridge_name, undefined, { ignorePunctuation: true, sensitivity: 'base' });
+ });
+
+ await this.bridgesQuery; // Wait for the bridges query to complete
+ }
+
+ openBridgePanel(bridge: {id: string, name: string}, isOwner: boolean=true ) {
+ const dialogRef = this.dialog.open(UpdateBridgeDialogComponent, { width: '90%',
+ maxHeight: '100vh',
+ maxWidth: '100vw',
+ autoFocus: false,
+ data: {
+ bridgeInfo: bridge,
+ asGroup: this.groupInfo?.id,
+ isOwner: isOwner,
+ },
+ });
+
+ dialogRef.afterClosed().subscribe((result: {success: boolean}) => {
+ if (result && result.success) {
+ this.updateBridges();
+ }
+ });
+ }
+
+ openBridgePanelFromConnection(connection: BridgeConnectionWithIconUrl) {
+ return this.openBridgePanel({
+ id: connection.conn.bridge_id,
+ name: connection.conn.bridge_name,
+ }, false);
+ }
+
+
+ async openProgram(program: ProgramMetadata): Promise {
+ let programType = 'scratch';
+
+ if (program.type === 'flow_program') {
+ programType = 'flow';
+ }
+ if (program.type === 'spreadsheet_program') {
+ programType = 'spreadsheet';
+ }
+
+ this.router.navigateByUrl(`/programs/${program.id}/${programType}`);
+ }
+
+ async enableProgram(program: ProgramMetadata) {
+ await this.programService.setProgramStatus(JSON.stringify({"enable": true}),
+ program.id);
+ program.enabled = true;
+ }
+
+ async archiveProgram(program: ProgramMetadata) {
+ await this.programService.setProgramStatus(JSON.stringify({"enable": false}),
+ program.id);
+ program.enabled = false;
+ delete this.programSettingsOpened[program.id];
+ }
+
+ async toggleShowProgramArchive(program: ProgramMetadata) {
+ if (this.programSettingsOpened[program.id] === 'archive') {
+ this.programSettingsOpened[program.id] = false;
+ }
+ else {
+ this.programSettingsOpened[program.id] = 'archive';
+ }
+ }
+
+ getEnabled(programs: ProgramMetadata[]): ProgramMetadata[] {
+ return programs.filter((p) => p.enabled);
+ }
+
+ getArchived(programs: ProgramMetadata[]): ProgramMetadata[] {
+ return programs.filter((p) => !p.enabled);
+ }
+
+ createGroup() {
+ this.router.navigate(['/new/group']);
+ }
+
+ openTutorial(tutorial: TutorialData) {
+ const win = this.browser.window.open(tutorial.url, '_blank');
+ win.focus();
+ }
+
+ openGroup(group: GroupInfo) {
+ this.router.navigateByUrl(`/groups/${group.canonical_name}`);
+ }
+
+ // Utils
+ readonly _roleToIcon = roleToIcon;
+
+ _toCapitalCase(x: string): string {
+ if (!x || x.length == 0) {
+ return x;
+ }
+ return x[0].toUpperCase() + x.substr(1);
+ }
+}
diff --git a/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.css b/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.css
new file mode 100644
index 00000000..aa121856
--- /dev/null
+++ b/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.css
@@ -0,0 +1,15 @@
+.bridge-name-input, .bridge-url-field {
+ width: 100%;
+}
+
+
+.accept-cancel {
+ margin-top: 2em;
+}
+
+.confirm-button {
+ background-color: #009688;
+ color: white;
+ font-weight: bold;
+ margin-right: 1ex;
+}
diff --git a/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.html b/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.html
new file mode 100644
index 00000000..8c16f389
--- /dev/null
+++ b/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.html
@@ -0,0 +1,42 @@
+Add bridge
+
+ Add a new bridge to the group.
+
+
+
+
+
+
+
+ Bridge name is required
+ Bridge name requires at least {{ options.controls.bridgeName.errors.minlength.requiredLength }} characters
+
+
+
+
+
+
+ Create
+
+
+
+
+
+ or
+
+
+
+
+ Go back
+
+
+
diff --git a/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.ts b/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.ts
new file mode 100644
index 00000000..427f671a
--- /dev/null
+++ b/frontend/src/app/dialogs/add-bridge-dialog/add-bridge-dialog.component.ts
@@ -0,0 +1,76 @@
+import { Component, Inject, ViewChild } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { MatButton } from '@angular/material/button';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { BridgeMetadata } from 'app/bridges/bridge';
+import { BridgeService } from 'app/bridges/bridge.service';
+import { GroupService } from 'app/group.service';
+import { SessionService } from 'app/session.service';
+
+@Component({
+ selector: 'app-add-bridge-dialog',
+ templateUrl: 'add-bridge-dialog.component.html',
+ styleUrls: [
+ 'add-bridge-dialog.component.css',
+ '../../libs/css/material-icons.css',
+ ],
+ providers: [BridgeService, SessionService, GroupService],
+})
+export class AddBridgeDialogComponent {
+ @ViewChild('confirmationButton') confirmationButton: MatButton;
+ bridgeControlUrl = "";
+ bridgeId: string | null;
+ bridgeCreated = false;
+
+ options: FormGroup;
+
+ constructor(public dialogRef: MatDialogRef,
+ private bridgeService: BridgeService,
+ private formBuilder: FormBuilder,
+
+ @Inject(MAT_DIALOG_DATA)
+ public data: { groupId?: string }) {
+ }
+
+ async ngOnInit() {
+ this.options = this.formBuilder.group({
+ bridgeName: ['', [Validators.required, Validators.minLength(4)]],
+ bridgeControlUrl: ['', []],
+ });
+ }
+
+ onBack(): void {
+ this.dialogRef.close({success: false, id: null, name: null});
+ }
+
+ create(): void {
+ const bridgeName = this.options.controls.bridgeName.value;
+
+ // Indicate that the process has started
+ const classList = this.confirmationButton._elementRef.nativeElement.classList;
+ classList.add('started');
+ classList.remove('completed');
+
+ this.bridgeCreated = true;
+
+ let createBridge: Promise;
+ if (this.data.groupId) {
+ createBridge = this.bridgeService.createGroupBridge(bridgeName, this.data.groupId);
+ }
+ else {
+ createBridge = this.bridgeService.createServicePort(bridgeName);
+ }
+
+ createBridge.then((bridgeMetadata: BridgeMetadata) => {
+ this.dialogRef.close({success: this.bridgeCreated, bridgeId: bridgeMetadata.id, bridgeName: bridgeName});
+ }).catch(() => {
+ this.bridgeCreated = false;
+ }).then(() => {
+ // Indicate that the process has ended
+ classList.remove('started');
+ classList.add('completed');
+ });
+
+ }
+
+}
diff --git a/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.component.ts b/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.component.ts
new file mode 100644
index 00000000..931999c0
--- /dev/null
+++ b/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.component.ts
@@ -0,0 +1,29 @@
+import { Component, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { VisibilityEnum } from 'app/program';
+
+@Component({
+ selector: 'app-change-program-visibility-dialog',
+ templateUrl: 'change-program-visibility-dialog.html',
+ styleUrls: [
+ 'change-program-visibility-dialog.scss',
+ '../../libs/css/material-icons.css',
+ ]
+})
+export class ChangeProgramVisilibityDialog {
+ visibility: VisibilityEnum;
+
+ constructor(public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA)
+ public data: { name: string, visibility: VisibilityEnum }) {
+ this.visibility = data.visibility;
+ }
+
+ onNoClick(): void {
+ this.dialogRef.close();
+ }
+
+ onConfirm(): void {
+ this.dialogRef.close({ visibility: this.visibility });
+ }
+}
diff --git a/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.html b/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.html
new file mode 100644
index 00000000..cac8e3cd
--- /dev/null
+++ b/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.html
@@ -0,0 +1,29 @@
+Update visibility of program {{ this.data.name }} ?
+
+
+ Set the program to:
+
+
+ public Public
+
+
+ link Share by link
+
+
+ lock Private
+
+
+
+
+ Cancel
+ Update
+
diff --git a/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.scss b/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.scss
new file mode 100644
index 00000000..72f58268
--- /dev/null
+++ b/frontend/src/app/dialogs/change-program-visibility-dialog/change-program-visibility-dialog.scss
@@ -0,0 +1,17 @@
+.program-name {
+ border-bottom: 2px solid #009688;
+ font-weight: bold;
+}
+
+.mat-dialog-actions > button.cancellation {
+ margin-left: auto;
+}
+
+mat-radio-button {
+ display: block;
+ margin-left: 2rem;
+}
+
+button {
+ right: 0;
+}
diff --git a/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.component.ts b/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.component.ts
new file mode 100644
index 00000000..bf3748cc
--- /dev/null
+++ b/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.component.ts
@@ -0,0 +1,265 @@
+import { Component, Inject, ViewChild } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatStepper } from '@angular/material/stepper';
+import { AssetService } from 'app/asset.service';
+import { SharedResource } from 'app/bridges/bridge';
+import { BridgeConnection } from 'app/connection';
+import { ConnectionService } from 'app/connection.service';
+import { EnvironmentService } from 'app/environment.service';
+import { UserGroupInfo } from 'app/group';
+import { GroupService } from 'app/group.service';
+import { getRequiredAssets, getRequiredBridges, transformProgram } from 'app/program-transformations';
+import { ProgramService } from 'app/program.service';
+import { Session } from 'app/session';
+import { SessionService } from 'app/session.service';
+import { getGroupPictureUrl, getUserPictureUrl } from 'app/utils';
+import { ProgramContent, ProgramMetadata } from '../../program';
+
+export type CloneProgramDialogComponentData = {
+ name: string,
+ program: ProgramContent,
+}
+
+@Component({
+ selector: 'app-clone-program-dialog',
+ templateUrl: 'clone-program-dialog.html',
+ styleUrls: [
+ 'clone-program-dialog.scss',
+ '../../libs/css/material-icons.css',
+ ]
+})
+export class CloneProgramDialogComponent {
+
+ // Models
+ destinationAccount: '__user' | string | null;
+ programNameFormGroup: FormGroup;
+ cloneToFormGroup: FormGroup;
+ bridgeConnectionFormGroup: FormGroup;
+ bridgesConnected = false;
+ loadingBridges = true;
+ links: {[key: string]: string} = {};
+ selectedGroup: UserGroupInfo | null;
+ programNameToSubmit: string;
+ usedLinks: { from: BridgeConnection, to: BridgeConnection }[] = [];
+
+ // Utils used on template
+ readonly _getUserPicture: (userId: string) => string;
+ readonly _getGroupPicture: (groupId: string) => string;
+
+ // Information
+ readonly sourceProgramId: string;
+ session: Session;
+ user_groups: UserGroupInfo[];
+ programBridges: string[];
+ connectionQuery: Promise;
+ usedBridges: BridgeConnection[];
+ existingBridges: BridgeConnection[];
+ cloningInProcess: boolean = false;
+ cloningDone: boolean = false;
+ createdProgramId: string;
+
+ // Views
+ @ViewChild('stepper') stepper: MatStepper;
+ sharedResourcesQuery: Promise;
+
+ constructor(private dialogRef: MatDialogRef,
+ environmentService: EnvironmentService,
+ sessionService: SessionService,
+ private groupService: GroupService,
+ private formBuilder: FormBuilder,
+ private programService: ProgramService,
+ private connectionService: ConnectionService,
+ private assetService: AssetService,
+
+ @Inject(MAT_DIALOG_DATA)
+ public data: CloneProgramDialogComponentData) {
+
+ this.sourceProgramId = data.program.id;
+
+ this._getUserPicture = getUserPictureUrl.bind(this, environmentService);
+ this._getGroupPicture = getGroupPictureUrl.bind(this, environmentService);
+
+ this.cloneToFormGroup = this.formBuilder.group({
+ cloneTo: new FormControl('', (control) => {
+ if (this.destinationAccount) {
+ return null;
+ }
+ else {
+ return { error: 'Not destination account selected' };
+ }
+ }),
+ });
+ this.bridgeConnectionFormGroup = this.formBuilder.group({
+ bridgeConnections: new FormControl('', (control) => {
+ if (this.bridgesConnected) {
+ return null;
+ }
+ else {
+ return { error: 'Not all bridges are connected' };
+ }
+ }),
+ });
+
+ this.programNameFormGroup = this.formBuilder.group({
+ programName: [data.name, [Validators.required, Validators.minLength(4)]],
+ });
+
+
+ sessionService.getSession().then(session => this.session = session );
+
+ this.groupService.getUserGroups()
+ .then(groups => this.user_groups = groups);
+
+ this.programBridges = getRequiredBridges(data.program);
+ this.connectionQuery = this.connectionService.getConnectionsOnProgram(data.program.id);
+ this.sharedResourcesQuery = this.programService.getProgramSharedResources(data.program.id);
+ }
+
+ updateDestinationAccount(value: string) {
+ this.destinationAccount = value;
+ this.cloneToFormGroup.get('cloneTo').updateValueAndValidity();
+ }
+
+ async getUsedBridges(): Promise {
+ const connections = await this.connectionQuery;
+ const sharedResources = await this.sharedResourcesQuery;
+ const usedOnProgram: BridgeConnection[] = [];
+
+ for (const conn of connections) {
+ if (this.programBridges.indexOf(conn.bridge_id) >= 0) {
+ usedOnProgram.push(conn);
+ }
+ }
+ for (const res of sharedResources) {
+ if (this.programBridges.indexOf(res.bridge_id) >= 0) {
+ usedOnProgram.push({
+ connection_id: null,
+ name: res.name,
+ icon: res.icon,
+ bridge_id: res.bridge_id,
+ bridge_name: res.name,
+ saving: null,
+ });
+ }
+ }
+
+ return usedOnProgram;
+ }
+
+ async prepareBridges() {
+ this.loadingBridges = true;
+ this.bridgesConnected = false;
+
+ if (this.destinationAccount === '__user') {
+ this.existingBridges = (await this.connectionService.getConnections());
+ this.selectedGroup = null;
+ }
+ else {
+ this.existingBridges = await this.connectionService.getConnectionsOnGroup(this.destinationAccount);
+ this.selectedGroup = this.user_groups.find(g => g.id === this.destinationAccount);
+
+ const sharedBridgesOnTarget = await this.groupService.getSharedResources(this.destinationAccount);
+ for (const share of sharedBridgesOnTarget) {
+ this.existingBridges.push({
+ connection_id: null,
+ name: share.name,
+ icon: share.icon,
+ bridge_id: share.bridge_id,
+ bridge_name: share.name,
+ saving: null,
+ });
+ }
+ }
+
+ this.usedBridges = await this.getUsedBridges();
+
+ for (const conn of this.usedBridges) {
+ let linkedTo: string | null = null;
+
+ const idIdx = this.existingBridges.findIndex((b) => b.bridge_id == conn.bridge_id );
+
+ if (idIdx >= 0) {
+ linkedTo = this.existingBridges[idIdx].bridge_id;
+ }
+
+ this.links[conn.bridge_id] = linkedTo;
+ }
+
+ this.loadingBridges = false;
+ this.bridgesConnected = this.usedBridges.length === 0;
+
+ this.onLinksUpdate();
+ }
+
+ onLinksUpdate() {
+ this.bridgesConnected = !Object.keys(this.links).some(id => !this.links[id]);
+
+ if (this.bridgesConnected) {
+ const usedLinks = [];
+ for (const srcBridge of this.usedBridges) {
+ const toBridge = this.links[srcBridge.bridge_id];
+
+ usedLinks.push({
+ from: srcBridge,
+ to: this.existingBridges.find(b => b.bridge_id === toBridge)
+ })
+ }
+ this.usedLinks = usedLinks;
+ }
+
+ this.bridgeConnectionFormGroup.get('bridgeConnections').updateValueAndValidity();
+ }
+
+ prepareSummary() {
+ this.programNameToSubmit = this.programNameFormGroup.get('programName').value;
+ }
+
+ async doClone() {
+ // Lock other steps
+ this.stepper.steps.forEach(step => step.editable = false);
+
+ // Start cloning
+ this.cloningInProcess = true;
+
+ const assets = getRequiredAssets(this.data.program);
+
+ // Change program to fit the new user
+ transformProgram(this.data.program, this.links);
+
+ // Create the program
+ let createdProgram: ProgramMetadata;
+ if (this.destinationAccount === '__user') {
+ createdProgram = await this.programService.createProgram(this.data.program.type, this.programNameToSubmit);
+ }
+ else {
+ createdProgram = await this.programService.createProgramOnGroup(this.data.program.type, this.programNameToSubmit, this.destinationAccount);
+ }
+
+ // Upload the program itself
+ const program = this.data.program;
+ program.id = createdProgram.id;
+
+ this.createdProgramId = program.id;
+
+ await this.programService.updateProgramById(program);
+
+ // Upload the required assets
+ const copies = assets.map(assetId =>
+ this.assetService.copyAssetToProgram(this.sourceProgramId, assetId, this.createdProgramId)
+ );
+
+ await Promise.all(copies);
+
+ this.cloningDone = true;
+ this.cloningInProcess = false;
+ }
+
+ onNoClick(): void {
+ this.dialogRef.close();
+ }
+
+ onConfirm(): void {
+ this.dialogRef.close({ success: true, program_id: this.createdProgramId });
+ }
+}
diff --git a/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.html b/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.html
new file mode 100644
index 00000000..22fe42d2
--- /dev/null
+++ b/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.html
@@ -0,0 +1,154 @@
+Cloning {{ data.name }} ...
+
+
+
+
+ check
+
+
+
+ Where to clone to?
+
+
+ My user
+
+
+ Group: {{ group.name }}
+
+
+ Cancel
+ Next
+
+
+
+ Connect to necessary bridges
+
+
+
+
+
+
+ This program doesn't use any bridge. You can go to the next step.
+
+
+
+
+ {{ conn.bridge_name }} keyboard_arrow_right
+
+ Bridge to link to
+
+
+ {{ to_bridge.bridge_name }}
+
+
+
+
+
+
+
+
+
+ Cancel
+ Back
+ Next
+
+
+
+ Name your new program
+
+
+
+
+
+ Cancel
+ Back
+ Next
+
+
+
+ Summary and confirmation
+
+
+
+
+ Cancel
+ Back
+ Clone
+ Go to cloned program
+
+
+
+
diff --git a/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.scss b/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.scss
new file mode 100644
index 00000000..11a53ca4
--- /dev/null
+++ b/frontend/src/app/dialogs/clone-program-dialog/clone-program-dialog.scss
@@ -0,0 +1,73 @@
+mat-dialog-content {
+ min-width: 50vw;
+}
+
+.stepper-buttons {
+ padding: 2rem;
+
+ button {
+ margin: 0.5rem;
+ }
+}
+
+.program-name {
+ border-bottom: 2px solid #009688;
+ font-weight: bold;
+}
+
+button {
+ right: 0;
+}
+
+/* Account selection */
+label.explanation {
+ display: block;
+ margin-bottom: 2rem;
+}
+
+mat-card {
+ display: inline-block;
+ vertical-align: bottom;
+ margin-left: 1ex;
+ margin-top: 1ex;
+ min-width: 3em;
+ text-align: center;
+ height: 4rem;
+ padding: 0 1rem 0 0;
+}
+
+mat-card.selected {
+ border: 1px solid #808;
+ box-shadow: 0px 2px 1px -1px rgba(200, 0, 200, 0.2), 0px 1px 1px 0px rgba(200, 0, 200, 0.14), 0px 1px 3px 0px rgba(200, 0, 200, 0.12);
+}
+
+.account-icon {
+ max-width: 10rem;
+ height: 4rem;
+ border-radius: 4px 0 0 4px;
+}
+
+.loading-spinner {
+ margin: 1rem auto;
+}
+
+ul.link-list mat-icon {
+ vertical-align: middle;
+}
+
+.summary-list {
+ .card-summary-item .summary-item-name {
+ height: 4rem;
+ display: inline-block;
+ vertical-align: initial;
+ line-height: 4rem;
+ }
+
+ li {
+ margin-bottom: 1rem;
+ }
+
+ .summary-item-name {
+ margin-right: 1rem;
+ }
+}
diff --git a/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.component.ts b/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.component.ts
new file mode 100644
index 00000000..6dd4bb16
--- /dev/null
+++ b/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.component.ts
@@ -0,0 +1,20 @@
+import { Component, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+@Component({
+ selector: 'app-confirm-delete-dialog',
+ templateUrl: 'confirm-delete-dialog.html',
+ styleUrls: [
+ 'confirm-delete-dialog.css',
+ ]
+})
+
+export class ConfirmDeleteDialogComponent {
+ constructor(public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA)
+ public data: { name: string }) {
+ }
+
+ onNoClick(): void {
+ this.dialogRef.close();
+ }
+}
diff --git a/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.css b/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.css
new file mode 100644
index 00000000..bd119ce2
--- /dev/null
+++ b/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.css
@@ -0,0 +1,12 @@
+.program-name {
+ border-bottom: 2px solid #fc3100;
+ font-weight: bold;
+}
+
+.mat-dialog-actions > button.cancellation {
+ margin-left: auto;
+}
+
+button {
+ right: 0;
+}
diff --git a/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.html b/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.html
new file mode 100644
index 00000000..782f75fa
--- /dev/null
+++ b/frontend/src/app/dialogs/confirm-delete-dialog/confirm-delete-dialog.html
@@ -0,0 +1,14 @@
+Are you sure you want to delete {{ this.data.name }} ?
+
+
+ Note that this change cannot be undone!
+
+
+ Cancel
+ Delete
+
diff --git a/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.html b/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.html
new file mode 100644
index 00000000..7f3556d6
--- /dev/null
+++ b/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.html
@@ -0,0 +1,40 @@
+
+
+
+ Loading available connections...
+
+
+
+
+
Connections complete
+
+ check All possible connections have been established
+
+
+ Close
+
+
+
+ 0)">
+
Select a bridge to connect to
+
+
+
+
+
+
+
+
+ {{ bridge.name }}
+
+
+
+
+
+
+
+ Cancel
+
+
diff --git a/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.scss b/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.scss
new file mode 100644
index 00000000..8530b6cd
--- /dev/null
+++ b/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.scss
@@ -0,0 +1,20 @@
+.mat-dialog-actions > button.cancellation {
+ margin-left: auto;
+}
+
+.info-msg {
+ text-align: center;
+ text-style: italics;
+}
+
+.text-icon {
+ vertical-align: text-bottom;
+}
+
+button {
+ right: 0;
+}
+
+mat-card > h4 {
+ word-break: break-all;
+}
diff --git a/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.ts b/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.ts
new file mode 100644
index 00000000..2b2668e4
--- /dev/null
+++ b/frontend/src/app/dialogs/connect-to-available-dialog/connect-to-available-dialog.component.ts
@@ -0,0 +1,62 @@
+import { Component, Inject } from '@angular/core';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { AddConnectionDialogComponent } from 'app/connections/add-connection-dialog.component';
+import { BridgeIndexData } from '../../bridges/bridge';
+import { BridgeService } from '../../bridges/bridge.service';
+import { ConnectionService } from '../../connection.service';
+import { ServiceService } from '../../service.service';
+import { SessionService } from '../../session.service';
+
+@Component({
+ selector: 'app-connect-to-available-dialog',
+ templateUrl: 'connect-to-available-dialog.component.html',
+ styleUrls: [
+ 'connect-to-available-dialog.component.scss',
+ '../../libs/css/material-icons.css',
+ ],
+ providers: [BridgeService, SessionService, ServiceService, ConnectionService],
+})
+export class ConnectToAvailableDialogComponent {
+ availableBridges: BridgeIndexData[] = null;
+
+ constructor(public dialogRef: MatDialogRef,
+ public sessionService: SessionService,
+ public serviceService: ServiceService,
+ public connectionService: ConnectionService,
+ public dialog: MatDialog,
+
+ @Inject(MAT_DIALOG_DATA)
+ public data: { groupId?: string }) {
+ if (!data) { data = this.data = {}; }
+
+
+ let query: Promise;
+ if (data.groupId) {
+ query = this.connectionService.getAvailableBridgesForNewConnectionOnGroup(data.groupId);
+ }
+ else {
+ query = this.connectionService.getAvailableBridgesForNewConnection();
+ }
+
+ query.then((bridges: BridgeIndexData[]) => {
+ this.availableBridges = bridges.sort((a, b) => {
+ return a.name.localeCompare(b.name, undefined, { ignorePunctuation: true, sensitivity: 'base' });
+ });
+ });
+ }
+
+ onNoClick(): void {
+ this.dialogRef.close({success: false});
+ }
+
+ enableService(bridge: BridgeIndexData): void {
+ const _dialogRef = this.dialog.open(AddConnectionDialogComponent, {
+ data: { groupId: this.data.groupId, bridgeInfo: bridge }
+ }).afterClosed().subscribe((result: {success: boolean}) => {
+ if (result && result.success) {
+ this.dialogRef.close({success: true});
+ }
+ });
+ }
+
+}
diff --git a/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.css b/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.css
new file mode 100644
index 00000000..d2af8c8e
--- /dev/null
+++ b/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.css
@@ -0,0 +1,9 @@
+.accept-cancel {
+ margin-top: 2em;
+}
+
+.confirm-button {
+ background-color: #009688;
+ color: white;
+ font-weight: bold;
+}
diff --git a/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.html b/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.html
new file mode 100644
index 00000000..207af9a0
--- /dev/null
+++ b/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.html
@@ -0,0 +1,25 @@
+Group collaborators
+
+ Invite some people to the group.
+
+
+
+
+
+
+ Update
+
+
+
+ or
+
+
+ Go back
+
+
diff --git a/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.ts b/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.ts
new file mode 100644
index 00000000..cfbeba5c
--- /dev/null
+++ b/frontend/src/app/dialogs/editor-collaborators-dialog/edit-collaborators-dialog.component.ts
@@ -0,0 +1,48 @@
+import { Component, Inject, ViewChild } from '@angular/core';
+import { MatButton } from '@angular/material/button';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { GroupCollaboratorEditorComponent } from 'app/components/group-collaborator-editor/group-collaborator-editor.component';
+import { GroupService } from 'app/group.service';
+import { SessionService } from 'app/session.service';
+
+@Component({
+ selector: 'app-edit-collaborators-dialog',
+ templateUrl: 'edit-collaborators-dialog.component.html',
+ styleUrls: [
+ 'edit-collaborators-dialog.component.css',
+ '../../libs/css/material-icons.css',
+ ],
+ providers: [SessionService, GroupService],
+})
+export class EditCollaboratorsDialogComponent {
+ @ViewChild('confirmationButton') confirmationButton: MatButton;
+ @ViewChild('groupCollaboratorEditor') groupCollaboratorEditor: GroupCollaboratorEditorComponent;
+
+ constructor(public dialogRef: MatDialogRef,
+ private groupService: GroupService,
+ private dialog: MatDialog,
+
+ @Inject(MAT_DIALOG_DATA)
+ public data: { groupId: string, existingCollaborators: { id: string }[] }) {
+ }
+
+ onNoClick(): void {
+ this.dialogRef.close({success: false});
+ }
+
+ async confirm(): Promise {
+ // Indicate that the process has started
+ const classList = this.confirmationButton._elementRef.nativeElement.classList;
+ classList.add('started');
+ classList.remove('completed');
+
+ // Perform update
+ await this.groupService.updateGroupCollaboratorList(this.data.groupId, this.groupCollaboratorEditor.getCollaborators());
+
+ // Indicate that the process has ended
+ classList.remove('started');
+ classList.add('completed');
+
+ this.dialogRef.close({success: true});
+ }
+}
diff --git a/frontend/src/app/dialogs/update-bridge-dialog/sliding-window.operator.ts b/frontend/src/app/dialogs/update-bridge-dialog/sliding-window.operator.ts
new file mode 100644
index 00000000..56ebbdc3
--- /dev/null
+++ b/frontend/src/app/dialogs/update-bridge-dialog/sliding-window.operator.ts
@@ -0,0 +1,15 @@
+import { pipe } from "rxjs";
+import { scan } from "rxjs/operators";
+
+export const slidingWindow = (maxSize: number) =>
+ pipe(
+ scan((acc, val) => {
+ if (acc.length >= maxSize) {
+ acc.splice(maxSize - 1);
+ }
+
+ acc.unshift(val);
+
+ return acc;
+ }, [])
+ );
diff --git a/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.css b/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.css
new file mode 100644
index 00000000..d018219b
--- /dev/null
+++ b/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.css
@@ -0,0 +1,308 @@
+h2 mat-icon {
+ vertical-align: sub;
+ margin-right: 1ex;
+}
+
+.bridge-name {
+ text-decoration: underline;
+}
+
+.accept-cancel {
+ margin-top: 2em;
+}
+
+.accept-cancel button {
+ margin-right: 1ex;
+}
+
+.confirm-button {
+ background-color: #009688;
+ color: white;
+ font-weight: bold;
+}
+
+button.toggle-fold {
+ min-width: 4ex;
+ padding: 0;
+}
+
+
+h4 button {
+ vertical-align: middle;
+}
+
+h4 .title {
+ margin-left: 0.5ex;
+}
+
+.conn-url {
+ margin-left: calc(1em + 2ex);
+ margin-bottom: 1ex;
+}
+
+.redacted {
+ text-decoration: underline dotted;
+}
+
+button.create-token {
+ margin-left: 1em;
+ padding: 0;
+ min-width: 5ex;
+}
+
+button.remove-token {
+ padding: 0;
+ min-width: 5ex;
+ background-color: #f44;
+ color: white;
+}
+
+.token .note {
+ text-decoration: none;
+ background: #444;
+ padding: 1ex;
+ color: white;
+ border-radius: 4px;
+ margin-left: 1em;
+ display: inline-block;
+}
+
+.token-creation-cell {
+ display: grid;
+ width: max-content;
+ margin-left: 1ex;
+}
+
+.token-creation-cell .token-name {
+ grid-row: 1;
+ grid-column: 1;
+ max-width: 40ex;
+}
+
+.token-creation-cell .error-message {
+ grid-row: 2;
+ grid-column-start: 1;
+ grid-column-end: 1;
+
+ margin-bottom: 2ex;
+}
+
+.token-creation-cell button {
+ grid-row: 1;
+ grid-column: 2;
+ height: 5ex;
+ margin-left: 1em;
+ margin-top: 1ex;
+ width: max-content;
+}
+
+span.value {
+ padding-left: 1ex;
+ text-decoration: underline dotted;
+}
+
+.expanded-true > .key button.toggle-fold .fold-open {
+ display: none;
+}
+
+.expanded-true > h4 button.toggle-fold .fold-open {
+ display: none;
+}
+
+.expanded-false > .key button.toggle-fold .fold-close {
+ display: none;
+}
+
+.expanded-false > h4 button.toggle-fold .fold-close {
+ display: none;
+}
+
+.expanded-false .token-list {
+ display: none;
+}
+
+.token-list {
+ padding-left: 1ex;
+ border-left: 1px solid rgba(0,0,0,0.3);
+ margin-left: 1em;
+}
+
+.token-list table {
+ margin-top: 1ex;
+ width: 100%;
+}
+
+.token-list th, .token-list td {
+ padding: 0.5ex 1ex 0.5ex 1ex;
+}
+
+.token-list tbody tr:nth-child(odd) {
+ background-color: #fed;
+}
+
+.token-list th.action-col, .token-list td.action-col {
+ border: none;
+}
+
+.token-value input {
+ text-decoration: underline rgba(0,0,0,0.3);
+}
+
+
+.expanded-false .expanded-contents {
+ display: none;
+}
+
+
+.key {
+ margin-bottom: 0.5rem;
+}
+
+.expanded-contents {
+ padding-left: 1ex;
+ border-left: 1px solid rgba(0,0,0,0.3);
+ margin-left: 1em;
+ margin-bottom: 0.5rem;
+}
+
+section {
+ margin-top: 1ex;
+}
+
+.resource-name {
+ font-size: 150%;
+ text-transform: capitalized;
+}
+
+ul.resource-options {
+ padding-left: 0;
+}
+
+.resource-options li {
+ margin-top: 1ex;
+}
+
+.confirm-cancel-shares, .confirm-cancel-logs {
+ padding: 1ex;
+ background-color: #eff;
+ border-radius: 4px;
+ border: 1px solid #055;
+ margin-bottom: 1em;
+}
+
+.confirm-cancel-shares button, .confirm-cancel-logs button {
+ margin: 0.5ex;
+}
+
+.save-share-button, .save-button {
+ background-color: #009688;
+ color: white;
+ font-weight: bold;
+}
+
+button.remove-share {
+ background-color: #f44;
+ color: white;
+ min-height: 3ex;
+ min-width: 5ex;
+ margin-left: 1ex;
+
+ padding: 0;
+ vertical-align: super;
+}
+
+button.remove-share mat-icon {
+ vertical-align: middle;
+}
+
+.shares .connector-text {
+ vertical-align: text-bottom;
+}
+
+.shares mat-form-field.shared-with {
+ vertical-align: middle;
+}
+
+.entry-name {
+ vertical-align: middle;
+ margin-left: 1ex;
+}
+
+.annotated-icon {
+ display: grid;
+}
+
+.annotated-icon mat-icon {
+ grid-row: 1;
+ grid-column: 1;
+ text-align: center;
+ width: 100%;
+}
+
+.annotated-icon label {
+ grid-row: 2;
+ grid-column: 1;
+ font-size: small;
+ line-height: initial;
+ margin: 0;
+ width: 100%;
+ text-align: center;
+}
+
+section.resources li .share-button {
+ margin: 0 1ex 0 1ex;
+ padding: 0.5ex;
+ min-width: 5ex;
+}
+
+.signal-terminal {
+ box-shadow: 0 0 2px 1px rgba(0,0,0,0.3) inset;
+ max-height: 50vh;
+ overflow: auto;
+}
+
+.signal-terminal .signal-message {
+ border-bottom: 1px solid rgba(0,0,0,0.3);
+ margin: 1ex 0 1ex 0;
+ padding: 1ex;
+}
+
+.signal-terminal .signal-message:last-child {
+ border: none;
+ padding: none;
+}
+
+.signal-terminal:empty::after {
+ content: "No messages yet";
+ font-style: italic;
+ display: inline-block;
+ padding: 1ex;
+}
+
+.save-signals-controls {
+ margin-bottom: 0.5rem;
+}
+
+.connection-name {
+ width: calc(100% - 11ex - 1ex);
+ display: inline-block;
+}
+
+.connection-name .content {
+ font-size: small;
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ vertical-align: sub;
+}
+
+.save-signals-controls > mat-slide-toggle {
+ margin-right: 1ex;
+ width: 11ex;
+}
+
+.connection-log-status {
+ font-size: small;
+ font-weight: bold;
+}
diff --git a/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.html b/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.html
new file mode 100644
index 00000000..23a46fef
--- /dev/null
+++ b/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.html
@@ -0,0 +1,316 @@
+
+ leak_add
+ {{ data.bridgeInfo.name }}
+
+
+
+
+
+ expand_more
+ unfold_less
+
+
+ Bridge Info
+
+
+
+
+ Connection URL:
+ {{ connectionUrl }}
+
+
+
+
+ 0 || null) && tokens.length">
+
+ 0" class="fold-open">expand_more
+ 0" class="fold-close">unfold_less
+
+ remove
+
+
+ Connection tokens
+
+
+ add
+
+
+
+
+
+
+
+
+
+ {{ saveTokenErrorMessage }}
+
+
+
+ save Save
+
+
+
+
+
+
+
+
+
+
+
+
+ 0" class="fold-open">expand_more
+ 0" class="fold-close">unfold_less
+
+ 0)" matTooltip="No resources found">panorama_fish_eye
+
+
+ Resources
+
+
+ 0" class="expanded-contents">
+
+
{{ resource.name }}
+
+
+
+
+
+ 0" class="fold-open">expand_more
+ 0" class="fold-close">unfold_less
+
+ remove
+
+
+
{{ entry.name }}
+
+
+
+ Share
+ share
+
+
+
+
+
+
+
+ Shared with
+
+
+ group
+
+
+ {{ group.name }}
+
+
+
+
+ delete
+
+
+
+
+
+
+
+
+
+ You've prepared some changes for the resource sharing.
+
+
+
+
+ clear
+ Cancel
+
+
+
+ save
+ Apply changes
+
+
+
+
+
+
+
+
+
+
+
+
+ expand_more
+ unfold_less
+
+ Signals
+
+
+
+
+
+
+
+ expand_more
+ unfold_less
+
+
+ Incoming Signals
+
+
+
+
+
+ {{ _stringify(signal, null, 4) }}
+
+
+
+
+
+
+
+
+
+ expand_more
+ unfold_less
+
+ Signal history
+
+
+
+
+
+
+ {{ conn.saving ? 'Log data' : "Don't log" }}
+
+
+
+
+ {{ conn.name }}
+
+
+
+
+
+
+
+ You've prepared some changes for the signal logging.
+
+
+
+
+ clear
+ Cancel
+
+
+
+ save
+ Apply changes
+
+
+
+
+
+
+
+ {{ _stringify(signal, null, 4) }}
+
+
+
+
+
+
+
+
+
+
+ Go back
+
+
+
+ Delete bridge
+
+
+
diff --git a/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.ts b/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.ts
new file mode 100644
index 00000000..af6def2d
--- /dev/null
+++ b/frontend/src/app/dialogs/update-bridge-dialog/update-bridge-dialog.component.ts
@@ -0,0 +1,369 @@
+import { Component, Inject, ViewChild } from '@angular/core';
+import { MatButton } from '@angular/material/button';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { BridgeIndexData, BridgeResource, BridgeResourceEntry, BridgeSignal, BridgeTokenInfo, FullBridgeTokenInfo, FullOwnerId } from 'app/bridges/bridge';
+import { BridgeService } from 'app/bridges/bridge.service';
+import { UserGroupInfo } from 'app/group';
+import { GroupService } from 'app/group.service';
+import { Session } from 'app/session';
+import { SessionService } from 'app/session.service';
+import { Observable } from 'rxjs';
+import { ConfirmDeleteDialogComponent } from '../confirm-delete-dialog/confirm-delete-dialog.component';
+import { slidingWindow } from './sliding-window.operator';
+import { Validators, FormBuilder, FormGroup } from '@angular/forms';
+import { MatTooltip } from '@angular/material/tooltip';
+import { MatSlideToggleChange } from '@angular/material/slide-toggle';
+import { ConnectionService } from 'app/connection.service';
+import { BridgeConnection } from 'app/connection';
+
+const INCOMING_SIGNAL_STREAM_LEN = 100;
+
+@Component({
+ selector: 'app-update-bridge-dialog',
+ templateUrl: 'update-bridge-dialog.component.html',
+ styleUrls: [
+ 'update-bridge-dialog.component.css',
+ '../../libs/css/material-icons.css',
+ ],
+ providers: [BridgeService, ConnectionService, GroupService, SessionService],
+})
+export class UpdateBridgeDialogComponent {
+ session: Session;
+ resources: BridgeResource[];
+ adminGroups: UserGroupInfo[];
+ groupsById: {[key: string]: UserGroupInfo};
+ groups: UserGroupInfo[];
+ expandedResources: {[key:string]: {[key: string]: boolean}} = {};
+ dirtyShares = false;
+ connectionUrl: string;
+
+ expandedBridgeInfo = true;
+ expandedResourceInfo = false;
+ expandedTokens = false;
+ tokens: (BridgeTokenInfo| FullBridgeTokenInfo)[];
+ options: FormGroup;
+ editableToken = false;
+ saveTokenErrorMessage: string;
+
+ _stringify = JSON.stringify;
+ private groupsReady: Promise;
+ @ViewChild('applyShareChanges') applyShareChanges: MatButton;
+ @ViewChild('resetShareChanges') resetShareChanges: MatButton;
+
+ expandedSignalInfo = false;
+ expandedIncomingSignals = false;
+ expandedHistoricSignals = false;
+ signalStream: Observable;
+ connections: (BridgeConnection & { savingInServer: boolean })[];
+
+ @ViewChild('updateSaveSignalsButton') updateSaveSignalsButton: MatButton;
+ lazyHistoricSignals = true;
+ signalHistory: any[];
+ dirtySaveLogs: boolean;
+
+ constructor(public dialogRef: MatDialogRef,
+ private bridgeService: BridgeService,
+ private sessionService: SessionService,
+ private groupService: GroupService,
+ private connectionService: ConnectionService,
+ private dialog: MatDialog,
+ private notification: MatSnackBar,
+ private formBuilder: FormBuilder,
+
+ @Inject(MAT_DIALOG_DATA)
+ public data: {
+ bridgeInfo: { id: string, name: string },
+ asGroup?: string,
+ isOwner: boolean,
+ }) {
+
+ this.options = this.formBuilder.group({
+ newTokenName: ['', [Validators.required, Validators.minLength(4)]],
+ });
+
+ if (data.isOwner) {
+ this.connectionUrl = bridgeService.getConnectionUrl(data.bridgeInfo.id);
+ this.bridgeService.getBridgeTokens(data.bridgeInfo.id, data.asGroup).then(tokens => this.tokens = tokens);
+ }
+
+ this.sessionService.getSession().then(session => {
+ this.session = session;
+
+ this.updateConnections();
+ this.resetShares();
+
+ const stream = this.bridgeService.getBridgeSignals(data.bridgeInfo.id, data.asGroup);
+ this.groupsReady = this.groupService.getUserGroups().then(groups => {
+ const acceptedGroups: UserGroupInfo[] = [];
+ for (const group of groups) {
+ if (group.role === 'admin') {
+ acceptedGroups.push(group);
+ }
+ }
+
+ const groupsById: {[key: string]: UserGroupInfo} = {};
+ for (const group of groups) {
+ groupsById[group.id] = group;
+ }
+
+ this.groups = groups;
+ this.groupsById = groupsById;
+ this.adminGroups = acceptedGroups;
+ })
+
+ this.signalStream = stream.pipe(
+ slidingWindow(INCOMING_SIGNAL_STREAM_LEN),
+ );
+ });
+ }
+
+ async ngOnInit() {
+ }
+
+ onBack(): void {
+ this.dialogRef.close({success: true});
+ }
+
+ toggleFold(resource: BridgeResource, entry: BridgeResourceEntry) {
+ if (!this.expandedResources[resource.name]) {
+ this.expandedResources[resource.name] = {};
+ }
+
+ this.expandedResources[resource.name][entry.id] = !this.expandedResources[resource.name][entry.id];
+ }
+
+ openFold(resource: BridgeResource, entry: BridgeResourceEntry) {
+ if (!this.expandedResources[resource.name]) {
+ this.expandedResources[resource.name] = {};
+ }
+
+ this.expandedResources[resource.name][entry.id] = true;
+ }
+
+ resetShares() {
+ this.bridgeService.getBridgeResources(this.data.bridgeInfo.id, this.data.asGroup).then(resources => {
+ this.resources = resources;
+ this.dirtyShares = false;
+ });
+ this.expandedResources = {};
+ }
+
+ async addShare(resource: BridgeResource, entry: BridgeResourceEntry) {
+ await this.groupsReady;
+
+ if (this.adminGroups.length === 0) {
+ this.notification.open('You need to be admin of a group to share a resource with it', 'ok', {
+ duration: 5000
+ });
+ }
+ if (!entry.shared_with) {
+ entry.shared_with = [];
+ }
+
+ // Find the first group, which doesn't have this resource shared already
+ const remainingGroups: UserGroupInfo[] = this.adminGroups.concat([]);
+ while (remainingGroups.length > 0){
+ const group = remainingGroups.shift();
+
+ if (!entry.shared_with.find((share) => share.id === group.id)) {
+ entry.shared_with.push({type: 'group', id: group.id});
+
+ this.openFold(resource, entry);
+ this.dirtyShares = true;
+
+ return;
+ }
+ }
+
+ if (remainingGroups.length === 0) {
+ this.notification.open('You are already sharing this resource with all your groups.' , 'ok', {
+ duration: 5000
+ });
+ }
+
+ }
+
+ removeShare(_resource: BridgeResource, entry: BridgeResourceEntry, index: number) {
+ entry.shared_with.splice(index, 1);
+ this.dirtyShares = true;
+ }
+
+
+ async applyShares() {
+ // Set state to in-progress
+ this.resetShareChanges.disabled = true;
+ const buttonClassList = this.applyShareChanges._elementRef.nativeElement.classList;
+ buttonClassList.add('started');
+ buttonClassList.remove('completed');
+
+ const operations : Promise[] = [];
+ for (const resource of this.resources) {
+ const connections: {[key: string]: {[key: string]: { name: string, shared_with: FullOwnerId[] }}} = {};
+ for(const value of resource.values) {
+ if (!connections[value.connection_id]) {
+ connections[value.connection_id] = {};
+ }
+
+ const val = {
+ name: value.name,
+ shared_with: value.shared_with,
+ };
+
+ connections[value.connection_id][value.id] = val;
+ }
+
+ for (const connectionId of Object.keys(connections)) {
+ const op = this.bridgeService.setShares(connectionId, resource.name, connections[connectionId], { asGroup: this.data.asGroup });
+ operations.push(op);
+ }
+ }
+
+ try {
+ await Promise.all(operations);
+ this.dirtyShares = false;
+ }
+ finally {
+ // Set state to "ready"
+ buttonClassList.remove('started');
+ buttonClassList.add('completed');
+ this.resetShareChanges.disabled = false;
+ }
+ }
+
+ deleteBridge() {
+ const dialogRef = this.dialog.open(ConfirmDeleteDialogComponent, {
+ data: this.data.bridgeInfo
+ });
+
+ dialogRef.afterClosed().subscribe(result => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ const deletion = (this.bridgeService.deleteBridge(this.data.bridgeInfo.id)
+ .catch(() => { return false; })
+ .then(success => {
+ if (!success) {
+ return;
+ }
+
+ this.dialogRef.close({success: true});
+ }));
+ });
+ }
+
+ removeToken(token: BridgeTokenInfo) {
+ const dialogRef = this.dialog.open(ConfirmDeleteDialogComponent, {
+ data: token
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ await this.bridgeService.revokeToken(this.data.bridgeInfo.id, token.name, this.data.asGroup);
+
+ const idx = this.tokens.indexOf(token);
+ this.tokens.splice(idx, 1);
+ });
+ }
+
+ createNewToken() {
+ this.editableToken = true;
+ this.expandedTokens = true;
+ }
+
+ async saveToken() {
+ const tokenName = this.options.controls.newTokenName.value;
+
+ let saved = false;
+ let tokenInfo: BridgeTokenInfo;
+ try {
+ tokenInfo = await this.bridgeService.createBridgeToken(this.data.bridgeInfo.id, tokenName, this.data.asGroup);
+ saved = true;
+ }
+ catch (err) {
+ if ((err.name === 'HttpErrorResponse') && (err.status === 409)) {
+ this.saveTokenErrorMessage = 'A token already exists with this name.';
+ this.expandedTokens = true;
+ }
+ else {
+ this.saveTokenErrorMessage = 'Error saving token. Try again later.';
+ }
+ }
+
+ if (saved){
+ this.options.controls.newTokenName.setValue('');
+ this.editableToken = false;
+ this.tokens.unshift(tokenInfo);
+
+ this.expandedTokens = true;
+ }
+ }
+
+
+ async updateConnections() {
+ let connectionQuery: Promise;
+ if (this.data.asGroup) {
+ connectionQuery = this.connectionService.getConnectionsOnGroup(this.data.asGroup);
+ }
+ else {
+ connectionQuery = this.connectionService.getConnections();
+ }
+
+ this.connections = (await connectionQuery)
+ .filter((c, _i, _a) => c.bridge_id === this.data.bridgeInfo.id)
+ .map((c, _i, _a) => {
+ (c as any).savingInServer = c.saving;
+ return c as BridgeConnection & { savingInServer: boolean };
+ });
+ this.dirtySaveLogs = false;
+ }
+
+ onChangeSaveSignals(connection: BridgeConnection, event: MatSlideToggleChange) {
+ connection.saving = !connection.saving;
+
+ this.dirtySaveLogs = this.connections.some(c => c.saving != c.savingInServer );
+ }
+
+ resetSaveLogs() {
+ this.connections.forEach(c => c.saving = c.savingInServer);
+ this.dirtySaveLogs = false;
+ }
+
+ toggleExpandHistoricSignals() {
+ this.expandedHistoricSignals = !this.expandedHistoricSignals;
+ if (this.expandedHistoricSignals) {
+ // Don't load historic signals until it's needed as it might contain a significant amount of data
+ if (this.lazyHistoricSignals) {
+ this.lazyHistoricSignals = false;
+ this.bridgeService.getBridgeHistoric(this.data.bridgeInfo.id, this.data.asGroup).then(history => this.signalHistory = history)
+ }
+ }
+ }
+
+ async updateSaveSignals() {
+ const buttonClass = this.updateSaveSignalsButton._elementRef.nativeElement.classList;
+ buttonClass.add('started');
+ buttonClass.remove('completed');
+
+ const updates = this.connections.map((conn) => {
+ const query = this.connectionService.setRecordConnectionsSignal(conn.connection_id, conn.saving, this.data.asGroup);
+ return query.then((res) => {
+ conn.savingInServer = conn.saving; // Update saving-in-server status
+ return res;
+ })
+ });
+
+ await Promise.all(updates);
+ this.dirtySaveLogs = false;
+
+ buttonClass.remove('started');
+ buttonClass.add('completed');
+ setTimeout(() => buttonClass.remove('completed'), 1000);
+ }
+}
diff --git a/frontend/src/app/environment.service.spec.ts b/frontend/src/app/environment.service.spec.ts
new file mode 100644
index 00000000..25893bf4
--- /dev/null
+++ b/frontend/src/app/environment.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { EnvironmentService } from './environment.service';
+
+describe('EnvironmentService', () => {
+ let service: EnvironmentService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(EnvironmentService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/environment.service.ts b/frontend/src/app/environment.service.ts
new file mode 100644
index 00000000..9e66055b
--- /dev/null
+++ b/frontend/src/app/environment.service.ts
@@ -0,0 +1,45 @@
+import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
+import { environment } from 'environments/environment';
+import { isPlatformServer } from '@angular/common';
+import { EnvironmentDefinition } from 'environments/environment-definition';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EnvironmentService {
+ environment: EnvironmentDefinition;
+
+ constructor(
+ @Inject(PLATFORM_ID) private platformId: Object
+ ) {
+ this.environment = environment;
+ }
+
+ private getApiHost(): string {
+ if (this.environment.SSRApiHost && isPlatformServer(this.platformId)) {
+ return this.environment.SSRApiHost;
+ }
+
+ return this.environment.ApiHost;
+ }
+
+ getApiRoot(): string {
+ return this.getApiHost() + '/api/v0';
+ }
+
+ getBrowserApiHost(): string {
+ return this.environment.ApiHost;
+ }
+
+ getBrowserApiRoot(): string {
+ return this.getBrowserApiHost() + '/api/v0';
+ }
+
+ hasYjsWsSyncServer(): boolean {
+ return !!this.environment.YjsWsSyncServer;
+ }
+
+ getYjsWsSyncServer(): string {
+ return this.environment.YjsWsSyncServer;
+ }
+}
diff --git a/frontend/src/app/flow-editor/atomic_flow_block.ts b/frontend/src/app/flow-editor/atomic_flow_block.ts
new file mode 100644
index 00000000..312dca66
--- /dev/null
+++ b/frontend/src/app/flow-editor/atomic_flow_block.ts
@@ -0,0 +1,1182 @@
+import { BlockManager } from './block_manager';
+import { Area2D, Direction2D, FlowBlock, FlowBlockOptions, InputPortDefinition, OutputPortDefinition, Position2D, FlowBlockData, FlowBlockInitOpts, BlockContextAction } from './flow_block';
+import { is_pulse } from './graph_transformations';
+import { FlowConnectionData, setConnectionType } from './flow_connection';
+import { EventEmitter } from 'events';
+import { FlowWorkspace } from './flow_workspace';
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+export type AtomicFlowBlockType = 'simple_flow_block';
+export const BLOCK_TYPE = 'simple_flow_block';
+
+const INPUT_PORT_REAL_SIZE = 10;
+const OUTPUT_PORT_REAL_SIZE = 10;
+const CONNECTOR_SIDE_SIZE = 15;
+const ICON_PADDING = '1ex';
+
+
+export type AtomicFlowBlockOperationType = 'operation' | 'getter' | 'trigger';
+
+export interface AtomicFlowBlockOptions extends FlowBlockOptions {
+ type: AtomicFlowBlockOperationType;
+ icon?: string,
+ block_function: string,
+ message: string;
+ key?: string;
+ subkey?: { "type": "argument", "index": number };
+ fixed_pulses?: boolean;
+}
+
+export interface AtomicFlowBlockData extends FlowBlockData {
+ type: AtomicFlowBlockType,
+ value: {
+ options: AtomicFlowBlockOptions,
+ slots: {[key: string]: string},
+
+ // This is used to indicate if a result of this block is used on another
+ // flow, and thus, if a signal reporting the result of this block has to be sent.
+ report_state?: boolean,
+
+ // These counts are needed to keep the consistency when linking
+ // inline arguments to it's ports
+ synthetic_input_count: number,
+ synthetic_output_count: number,
+ },
+}
+
+type NamedVarMessageChunk = { type: 'named_var', val: string, name: string };
+type MessageChunk = ( { type: 'const', val: string }
+ | NamedVarMessageChunk
+ | { type: 'index_var', index: number, direction: 'in' | 'out' }
+ );
+
+function is_digit(char: string): boolean {
+ switch (char) {
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+export function isAtomicFlowBlockOptions(opt: FlowBlockOptions): opt is AtomicFlowBlockOptions {
+ return ((opt as AtomicFlowBlockOptions).type === 'operation'
+ || (opt as AtomicFlowBlockOptions).type === 'getter'
+ || (opt as AtomicFlowBlockOptions).type === 'trigger');
+}
+
+export function isAtomicFlowBlockData(opt: FlowBlockData): opt is AtomicFlowBlockData {
+ return opt.type === BLOCK_TYPE;
+}
+
+function parse_chunks(message: string): MessageChunk[] {
+ const result: MessageChunk[] = [];
+
+ let currentChunk = [];
+ let currentChunkType: 'text' | 'named_var' | 'index_var' = 'text';
+ let currentDirection: 'in' | 'out' = null;
+
+ for (let index=0; index < message.length; index++) {
+ if (currentChunkType === 'text') {
+ if (message[index] != '%') {
+ currentChunk.push(message[index]);
+ }
+ else {
+ if (((index + 1) >= message.length) || ('(io'.indexOf(message[index+1]) < 0)) {
+ // Cannot continue '%(' for named, '%i' or '%o' for indexed
+ currentChunk.push(message[index]);
+ }
+ else if (message[index+1] === 'i' || message[index+1] === 'o') {
+ const name = currentChunk.join('');
+ result.push({type: 'const', val: name });
+ currentChunk = [];
+ currentChunkType = 'index_var';
+
+ if (message[index+1] === 'i') {
+ currentDirection = 'in';
+ }
+ else {
+ currentDirection = 'out';
+ }
+ index++;
+ }
+ else {
+ const name = currentChunk.join('');
+ result.push({type: 'const', val: name });
+ currentChunk = [];
+ currentChunkType = 'named_var';
+ index++;
+ }
+ }
+ }
+ else if (currentChunkType === 'index_var') {
+ if (is_digit(message[index])) {
+ currentChunk.push(message[index]);
+ }
+ else {
+ if (currentChunk) {
+ result.push({
+ type: 'index_var',
+ index: parseInt(currentChunk.join('')) - 1, // Account for 1-index in message
+ direction: currentDirection
+ });
+ currentChunk = [];
+ currentChunkType = 'text';
+ index--;
+ }
+ else {
+ throw new Error(`Unclosed indexed argument '%${currentDirection[0]}. Expected number, found ${message[index]}`);
+ }
+ }
+ }
+ else {
+ if (message[index] == ')') {
+ const name = currentChunk.join('');
+ result.push({ type: 'named_var', val: name, name: name });
+ currentChunk = [];
+ currentChunkType = 'text';
+ }
+ else {
+ currentChunk.push(message[index]);
+ }
+ }
+ }
+
+ if (currentChunkType === 'text') {
+ if (currentChunk) {
+ result.push({type: 'const', val: currentChunk.join('')});
+ }
+ }
+ else if (currentChunkType === 'index_var'){
+ if (currentChunk) {
+ result.push({
+ type: 'index_var',
+ index: parseInt(currentChunk.join('')) - 1, // Account for 1-index in message
+ direction: currentDirection
+ });
+ }
+ else {
+ throw new Error("Unclosed indexed argument '%' (expected number)");
+ }
+ }
+ else {
+ throw new Error("Unclosed tag: %(" + currentChunk.join(''));
+ }
+
+ return result;
+}
+
+export class AtomicFlowBlock implements FlowBlock {
+ readonly id: string;
+ readonly onMoveCallbacks: ((pos: Position2D) => void)[] = [];
+ private _workspace: FlowWorkspace;
+
+ options: AtomicFlowBlockOptions;
+ overridenInputTypes: ('user-pulse' | 'pulse')[] = [];
+ overridenOutputTypes: ('user-pulse' | 'pulse')[] = [];
+
+ synthetic_input_count = 0;
+ synthetic_output_count = 0;
+ namedChunkTextBoxes: {[key: string]: SVGTextElement } = {};
+
+ constructor(options: AtomicFlowBlockOptions, blockId: string, synthetic_input_count?: number, synthetic_output_count?: number) {
+ this.id = blockId;
+ if (!(options.message)) {
+ throw new Error("'message' property is required to create a block");
+ }
+
+ if (synthetic_input_count) {
+ this.synthetic_input_count = synthetic_input_count;
+ }
+
+ if (synthetic_output_count) {
+ this.synthetic_output_count = synthetic_output_count;
+ }
+
+ [this.options, this.synthetic_input_count, this.synthetic_output_count ] = AtomicFlowBlock.add_synth_io(options,
+ synthetic_input_count,
+ synthetic_output_count);
+ this.options.on_io_selected = options.on_io_selected;
+ this.options.on_dropdown_extended = options.on_dropdown_extended;
+ this.options.on_inputs_changed = options.on_inputs_changed;
+
+ this.input_groups = [];
+ this.output_groups = [];
+ this.input_count = [];
+
+
+ this.chunks = parse_chunks(this.options.message);
+ for (const chunk of this.chunks) {
+ if (chunk.type === 'named_var') {
+ if (options.slots && options.slots[chunk.name]) {
+ chunk.val = options.slots[chunk.name];
+ }
+ }
+ }
+
+ this.chunkBoxes = [];
+ }
+
+ public dispose() {
+ this.canvas.removeChild(this.group);
+ }
+
+ public static add_synth_io(options: AtomicFlowBlockOptions,
+ synthetic_input_count?: number,
+ synthetic_output_count?: number): [AtomicFlowBlockOptions, number, number] {
+ synthetic_input_count = synthetic_input_count || 0;
+ synthetic_output_count = synthetic_output_count || 0;
+
+ options = JSON.parse(JSON.stringify(options));
+
+ // Update inputs
+ if (!options.inputs) {
+ options.inputs = [];
+ }
+
+ // Update outputs
+ if (!options.outputs) {
+ options.outputs = [];
+ }
+
+ if (!synthetic_input_count && AtomicFlowBlock.required_synth_inputs(options) > 0) {
+ options.inputs = ([ { type: "pulse" } ] as InputPortDefinition[]).concat(options.inputs);
+ synthetic_input_count++;
+ }
+
+ if (!synthetic_output_count && AtomicFlowBlock.required_synth_outputs(options) > 0) {
+ options.outputs = ([ { type: "pulse" } ] as OutputPortDefinition[]).concat(options.outputs);
+ synthetic_output_count++;
+ }
+
+ return [options, synthetic_input_count, synthetic_output_count];
+ }
+
+ public static required_synth_outputs(options: AtomicFlowBlockOptions): number {
+ let num_outputs = 0;
+
+ if (options.type !== 'getter') {
+ let has_pulse_output = false;
+ for (const output of options.outputs || []) {
+ if (is_pulse(output)) {
+ has_pulse_output = true;
+ break;
+ }
+ }
+
+ if (!has_pulse_output) {
+ num_outputs = 1;
+ }
+ }
+
+ return num_outputs;
+ }
+
+ public static required_synth_inputs(options: AtomicFlowBlockOptions): number {
+ let num_inputs = 0;
+
+ if (['trigger', 'getter'].indexOf(options.type) < 0) {
+ let has_pulse_input = false;
+ for (const input of options.inputs || []) {
+ if (!input) {
+ throw new Error(`Empty input on ${options.inputs}`);
+ }
+
+ if (is_pulse(input)) {
+ has_pulse_input = true;
+ break;
+ }
+ }
+
+ if (!has_pulse_input) {
+ num_inputs++;
+ }
+ }
+
+ return num_inputs;
+ }
+
+ // Render elements
+ private group: SVGGElement;
+ private node: SVGGElement;
+ private rect: SVGRectElement;
+ private rectShadow: SVGRectElement;
+ private canvas: SVGElement;
+ private icon: SVGImageElement;
+ private iconPlate: SVGRectElement;
+ private iconSeparator: SVGPathElement;
+
+ private chunkGroup: SVGGElement;
+ private chunkBoxes: SVGElement[];
+ private chunks: MessageChunk[];
+
+ private position: {x: number, y: number};
+ private input_count: number[];
+
+ private input_x_position: number;
+ private output_x_position: number;
+
+ // I/O groups
+ private input_groups: SVGElement[];
+ private output_groups: SVGElement[];
+
+ public static GetBlockType(): string {
+ return BLOCK_TYPE;
+ }
+
+ public serialize(): AtomicFlowBlockData {
+ return {
+ type: BLOCK_TYPE,
+ value: {
+ options: JSON.parse(JSON.stringify(this.options)),
+ slots: this.getSlots(),
+ synthetic_input_count: this.synthetic_input_count,
+ synthetic_output_count: this.synthetic_output_count,
+ },
+ }
+ }
+
+ public static Deserialize(data: AtomicFlowBlockData, blockId: string, manager: BlockManager): FlowBlock {
+ if (data.type !== BLOCK_TYPE){
+ throw new Error(`Block type mismatch, expected ${BLOCK_TYPE} found: ${data.type}`);
+ }
+
+ const options: AtomicFlowBlockOptions = JSON.parse(JSON.stringify(data.value.options));
+ options.on_dropdown_extended = manager.onDropdownExtended.bind(manager);
+ options.on_inputs_changed = manager.onInputsChanged.bind(manager);
+ options.on_io_selected = manager.onIoSelected.bind(manager);
+
+ const block = new AtomicFlowBlock(options,
+ blockId,
+ data.value.synthetic_input_count,
+ data.value.synthetic_output_count,
+ );
+
+ for (const slot of Object.keys(data.value.slots || {})) {
+ const chunk = block.chunks.find((val) => val.type === 'named_var' && val.name === slot );
+ block.updateChunk(chunk, data.value.slots[slot]);
+ }
+
+ return block;
+ }
+
+ public getBodyElement(): SVGGraphicsElement {
+ if (!this.group) {
+ throw Error("Not rendered");
+ }
+
+ return this.group;
+ }
+
+ public getBodyArea(): Area2D {
+ const rect = (this.group as any).getBBox();
+ return {
+ x: this.position.x,
+ y: this.position.y,
+ width: rect.width,
+ height: rect.height,
+ }
+ }
+
+ public getOffset(): {x: number, y: number} {
+ return {x: this.position.x, y: this.position.y};
+ }
+
+ public moveTo(pos: Position2D) {
+ this.position.x = pos.x;
+ this.position.y = pos.y;
+
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+ }
+
+ public updateOptions(blockData: FlowBlockData): void {
+ const data = blockData as AtomicFlowBlockData;
+ for (const var_name of Object.keys(data.value.slots)) {
+ const value = data.value.slots[var_name];
+ const chunk = this.chunks.find(p => p.type === 'named_var' && p.name == var_name);
+ if (!chunk) {
+ console.error(`Chunk not found for updating. Expected name: ${var_name}. New value: ${value}.`);
+ continue;
+ }
+
+ const prevValue = (chunk as NamedVarMessageChunk).val;
+
+ if (this._workspace) {
+ this._workspace._notifyChangedVariable(prevValue, value);
+ }
+
+ this.updateChunk(chunk, value);
+ }
+ }
+
+ private onOptionsUpdate() {
+ if (this._workspace) {
+ this._workspace.onBlockOptionsChanged(this);
+ }
+ }
+
+ public moveBy(distance: {x: number, y: number}): FlowBlock[] {
+ if (!this.group) {
+ throw Error("Not rendered");
+ }
+
+ this.position.x += distance.x;
+ this.position.y += distance.y;
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+
+ for (const callback of this.onMoveCallbacks) {
+ callback(this.position);
+ }
+
+ return [];
+ }
+
+ public onMove(callback: (pos: Position2D) => void) {
+ this.onMoveCallbacks.push(callback);
+ }
+
+ public endMove(): FlowBlock[] {
+ return [];
+ }
+
+ public onGetFocus() {}
+ public onLoseFocus() {}
+
+ public getPositionOfInput(index: number, edge?: boolean): Position2D {
+ const group = this.input_groups[index];
+ const circle = group.getElementsByTagName('circle')[0];
+
+ const position = { x: parseInt(circle.getAttributeNS(null, 'cx')),
+ y: parseInt(circle.getAttributeNS(null, 'cy')),
+ };
+
+ if (edge) {
+ position.y -= INPUT_PORT_REAL_SIZE;
+ }
+
+ return position;
+ }
+
+ public getPositionOfOutput(index: number, edge?: boolean): Position2D {
+ const group = this.output_groups[index];
+ const circle = group.getElementsByTagName('circle')[0];
+ const position = { x: parseInt(circle.getAttributeNS(null, 'cx')),
+ y: parseInt(circle.getAttributeNS(null, 'cy')),
+ };
+
+ if (edge) {
+ position.y += OUTPUT_PORT_REAL_SIZE;
+ }
+
+ return position;
+ }
+
+ public addConnection(direction: 'in' | 'out', input_index: number, _block: FlowBlock, sourceType: string): boolean {
+ if (direction === 'out') { return false; }
+
+ if (!this.input_count[input_index]) {
+ this.input_count[input_index] = 0;
+ }
+ this.input_count[input_index]++;
+
+ const extra_opts = this.options.extra_inputs;
+ if (extra_opts) {
+ // Consider need for extra inputs
+ let has_available_inputs = false;
+ for (let i = 0; i < this.options.inputs.length; i++) {
+ if (!this.input_count[i]) {
+ has_available_inputs = true;
+ break;
+ }
+ }
+
+ if (!has_available_inputs) {
+ // No available inputs, *might* need to create some more
+
+ if ((extra_opts.quantity === 'any')
+ || (extra_opts.quantity.max < this.input_groups.length)) {
+
+ // Create new input
+ let input_index = this.input_groups.length;
+
+ const input = { type: extra_opts.type };
+
+ this.addInput(input, input_index);
+ this.options.inputs.push(input);
+ this.updateBody();
+ if (this.options.on_inputs_changed) {
+ this.options.on_inputs_changed(this, input_index);
+ }
+ }
+ }
+ }
+
+ // Consider updating the output pulse type
+ const changedOutput = this.refreshInputConnection(input_index, sourceType);
+
+ return changedOutput;
+ }
+
+ public removeConnection(direction: 'in' | 'out', index: number): boolean {
+ if (direction === 'out') { return; }
+
+ if (this.input_count[index]) {
+ this.input_count[index]--;
+ }
+
+
+ // Consider updating the output pulse type
+ const origType = this.options.inputs[index].type as ('pulse' | 'user-pulse');
+ const changedOutput = this.refreshInputConnection(index, origType);
+
+ return changedOutput;
+ }
+
+ public refreshConnectionTypes(linksFrom: [FlowConnectionData, SVGElement][],
+ linksTo: [FlowConnectionData, SVGElement][],
+ ) {
+ for (const [link, _element] of linksTo) {
+ const index = link.sink.input_index;
+
+ const linkType = link.type;
+ this.refreshInputConnection(index, linkType);
+ }
+
+ for (const [link, element] of linksFrom) {
+ const idx = link.source.output_index;
+
+ if (this.overridenOutputTypes[idx]) {
+ setConnectionType(this.overridenOutputTypes[idx], link, element);
+ }
+ }
+ }
+
+ private refreshInputConnection(index: number, linkType: string) : boolean {
+ let changedOutput = false;
+
+ if ((!this.options.fixed_pulses)
+ && is_pulse(this.options.inputs[index])
+ && ((linkType === 'pulse') || (linkType === 'user-pulse'))
+ ) {
+ this.setInPulseType(index, linkType);
+
+ for (let outIndex = 0;
+ (this.options.outputs
+ && outIndex < this.options.outputs.length);
+ outIndex++) {
+
+ if (is_pulse(this.options.outputs[outIndex])) {
+ this.setOutPulseType(outIndex, linkType);
+
+ changedOutput = true;
+ }
+ }
+
+ }
+ return changedOutput;
+ }
+
+ private setInPulseType(inputIndex: number, sourceType: 'pulse' | 'user-pulse') {
+ const inClass = this.input_groups[inputIndex].getElementsByClassName('external_port')[0].classList;
+ inClass.remove('pulse_port');
+ inClass.remove('user-pulse_port');
+
+ inClass.add(sourceType + '_port');
+
+ this.overridenInputTypes[inputIndex] = sourceType;
+ }
+
+ private setOutPulseType(outputIndex: number, sourceType: 'pulse' | 'user-pulse') {
+ const outClass = this.output_groups[outputIndex].getElementsByClassName('external_port')[0].classList;
+ outClass.remove('pulse_port');
+ outClass.remove('user-pulse_port');
+
+ outClass.add(sourceType + '_port');
+
+ this.overridenOutputTypes[outputIndex] = sourceType;
+ }
+
+ private addInput(input: InputPortDefinition, index: number) {
+ const inputs_x_margin = 10; // px
+ const input_plating_x_margin = 3; // px
+
+ const in_group = document.createElementNS(SvgNS, 'g');
+ in_group.classList.add('input');
+ this.group.appendChild(in_group);
+
+ const port_external = document.createElementNS(SvgNS, 'circle');
+ const port_internal = document.createElementNS(SvgNS, 'circle');
+
+ in_group.appendChild(port_external);
+ in_group.appendChild(port_internal);
+
+ const input_port_size = 50;
+ const input_port_internal_size = 5;
+ const input_position_start = this.input_x_position;
+ let input_position_end = this.input_x_position + input_port_size;
+
+ if (input.name) {
+ // Bind input name and port
+ const port_plating = document.createElementNS(SvgNS, 'rect');
+ in_group.appendChild(port_plating);
+
+ const text = document.createElementNS(SvgNS, 'text');
+ text.textContent = input.name;
+ text.setAttributeNS(null, 'class', 'argument_name input');
+ in_group.appendChild(text);
+
+ input_position_end = Math.max(input_position_end, (this.input_x_position
+ + text.getBoundingClientRect().width
+ + input_plating_x_margin * 2));
+ const input_width = input_position_end - input_position_start;
+
+ text.setAttributeNS(null, 'x', input_position_start + input_width/2 - text.getBoundingClientRect().width/2 + '');
+ text.setAttributeNS(null, 'y', (INPUT_PORT_REAL_SIZE + text.getBoundingClientRect().height/3) + '' );
+
+ this.input_x_position = input_position_end + inputs_x_margin;
+
+ const input_height = Math.max(input_port_size / 2, (INPUT_PORT_REAL_SIZE
+ + text.getBoundingClientRect().height));
+
+ // Configure port connector now that we know where the input will be positioned
+ port_plating.setAttributeNS(null, 'class', 'port_plating');
+ port_plating.setAttributeNS(null, 'x', input_position_start + '');
+ port_plating.setAttributeNS(null, 'y', '0'); // Node stroke-width /2
+ port_plating.setAttributeNS(null, 'width', (input_position_end - input_position_start) + '');
+ port_plating.setAttributeNS(null, 'height', input_height/1.5 + '');
+ }
+ else {
+ this.input_x_position += input_port_size;
+ }
+
+ let type_class = 'unknown_type';
+ if (input.type) {
+ type_class = input.type + '_port';
+ }
+
+ // Draw the input port
+ const port_x_center = (input_position_start + input_position_end) / 2;
+ const port_y_center = 0;
+
+ port_external.setAttributeNS(null, 'class', 'input external_port ' + type_class);
+ port_external.setAttributeNS(null, 'cx', port_x_center + '');
+ port_external.setAttributeNS(null, 'cy', port_y_center + '');
+ port_external.setAttributeNS(null, 'r', INPUT_PORT_REAL_SIZE + '');
+
+ port_internal.setAttributeNS(null, 'class', 'input internal_port');
+ port_internal.setAttributeNS(null, 'cx', port_x_center + '');
+ port_internal.setAttributeNS(null, 'cy', port_y_center + '');
+ port_internal.setAttributeNS(null, 'r', input_port_internal_size + '');
+
+ if (this.options.on_io_selected) {
+ const element_index = index; // Capture for use in callback
+ in_group.onclick = ((_ev: MouseEvent) => {
+ this.options.on_io_selected(this, 'in', element_index, input,
+ { x: port_x_center, y: port_y_center });
+ });
+ }
+
+ this.input_groups[index] = in_group;
+ }
+
+ private updateChunk(chunk: MessageChunk, new_value: string) {
+ if (chunk.type === 'const') {
+ console.warn('Constant value chunks cannot be updated');
+ return;
+ }
+ if (chunk.type === 'index_var') {
+ console.warn('Indexed value chunks cannot be updated');
+ return;
+ }
+
+ chunk.val = new_value;
+ if (this.namedChunkTextBoxes[chunk.name]) {
+ // Might not exist before initialization
+ this.namedChunkTextBoxes[chunk.name].textContent = new_value;
+ this.updateBody();
+ }
+ }
+
+ private updateBody() {
+ const MIN_WIDTH = 100;
+ const X_PADDING = 5; // px
+ const PLATE_X_PADDING = 2; // px
+ const IMAGE_X_PADDING = 2; // px
+
+ let x_offset = 0;
+ if (this.icon) {
+ const icon_rect = this.icon.getBBox();
+ x_offset += icon_rect.width + icon_rect.x * 2;
+ }
+
+
+ let chunks_width = 0;
+ for (let i = 0; i < this.chunks.length; i++) {
+ if (this.chunks[i].type === 'const') {
+ chunks_width += this.chunkBoxes[i].getBoundingClientRect().width + X_PADDING;
+ }
+ else if (this.chunks[i].type === 'named_var') {
+ const group = this.chunkBoxes[i];
+ const image = group.getElementsByClassName('var_dropdown_icon')[0];
+
+ const text = group.getElementsByClassName('var_name')[0];
+ chunks_width += text.getBoundingClientRect().width + image.getBoundingClientRect().width
+ + X_PADDING + PLATE_X_PADDING * 2 + IMAGE_X_PADDING * 2;
+ }
+ else if (this.chunks[i].type === 'index_var') {
+ chunks_width += CONNECTOR_SIDE_SIZE + X_PADDING + PLATE_X_PADDING * 2;
+ }
+ }
+
+ let widest_section = MIN_WIDTH;
+ widest_section = Math.max(widest_section, chunks_width + X_PADDING * 2);
+
+ // Both input and output already accout for the x_offset
+ widest_section = Math.max(widest_section, this.input_x_position - x_offset);
+ widest_section = Math.max(widest_section, this.output_x_position - x_offset);
+
+ let next_chunk_position = x_offset + widest_section / 2 - chunks_width / 2;
+ for (let i = 0; i < this.chunks.length; i++) {
+
+ const chunk = this.chunks[i];
+ if (chunk.type === 'const') {
+ this.chunkBoxes[i].setAttributeNS(null, 'x', next_chunk_position + '');
+ next_chunk_position += this.chunkBoxes[i].getBoundingClientRect().width + X_PADDING;
+ }
+ else if (chunk.type === 'named_var') {
+ const group = this.chunkBoxes[i];
+ const text = group.getElementsByClassName('var_name')[0];
+ const plate = group.getElementsByClassName('var_plate')[0];
+ const image = group.getElementsByClassName('var_dropdown_icon')[0];
+
+ text.setAttributeNS(null, 'x', next_chunk_position + PLATE_X_PADDING * 2
+ + '');
+
+ const text_width = text.getBoundingClientRect().width;
+
+ image.setAttributeNS(null, 'x', next_chunk_position + PLATE_X_PADDING * 2 + text_width + IMAGE_X_PADDING
+ + '');
+
+ const image_width = image.getBoundingClientRect().width;
+
+ plate.setAttributeNS(null, 'x', next_chunk_position + '');
+ plate.setAttributeNS(null, 'width', text_width + image_width + PLATE_X_PADDING * 2 + IMAGE_X_PADDING * 2 + "");
+
+ next_chunk_position += text_width + image_width + X_PADDING + PLATE_X_PADDING * 2 + IMAGE_X_PADDING * 2;
+ }
+ else if (chunk.type === 'index_var') {
+ const group = this.chunkBoxes[i];
+ const connector = group.getElementsByClassName('var_connector')[0];
+ const path = group.getElementsByClassName('var_path')[0];
+
+ connector.setAttributeNS(null, 'x', next_chunk_position + '');
+ connector.setAttributeNS(null, 'width', CONNECTOR_SIDE_SIZE + "");
+
+ let target: Position2D;
+ if (chunk.direction === 'in'){
+ target = this.getPositionOfInput(chunk.index + this.synthetic_input_count);
+ target.y += INPUT_PORT_REAL_SIZE / 2;
+ }
+ else if (chunk.direction === 'out'){
+ target = this.getPositionOfOutput(chunk.index + this.synthetic_output_count);
+ target.y -= OUTPUT_PORT_REAL_SIZE / 2;
+ }
+
+ const conn_area = (connector as any).getBBox() as Area2D;
+ let off: Position2D;
+ if (conn_area.y > target.y) { // Under input, start on connector's top
+ off = {
+ x: conn_area.x + conn_area.width / 2,
+ y: conn_area.y,
+ };
+ }
+ else { // Over output, start on connector's bottom
+ off = {
+ x: conn_area.x + conn_area.width / 2,
+ y: conn_area.y + conn_area.height,
+ };
+ }
+
+ path.setAttributeNS(null, 'd', `M${off.x},${off.y} ${target.x},${target.y}`);
+
+ next_chunk_position += CONNECTOR_SIDE_SIZE + X_PADDING + PLATE_X_PADDING * 2;
+ }
+ }
+
+ const box_width = widest_section + x_offset;
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+ this.rectShadow.setAttributeNS(null, 'width', box_width + "");
+ }
+
+ public getBlockContextActions(): BlockContextAction[] {
+ return [];
+ }
+
+ public getSlots(): {[key: string]: string} {
+ const slots: {[key: string]: string} = {};
+ for (const chunk of this.chunks) {
+ if (chunk.type === 'named_var') {
+ slots[chunk.name] = chunk.val;
+ }
+ }
+
+ return slots;
+ }
+
+ public getInputs(): InputPortDefinition[] {
+ if (!this.options.inputs) { return []; }
+ return JSON.parse(JSON.stringify(this.options.inputs));
+ }
+
+ public getOutputType(index: number): string {
+ if (this.overridenOutputTypes[index]) {
+ return this.overridenOutputTypes[index];
+ }
+
+ return this.options.outputs[index].type;
+ }
+
+ public getInputType(index: number): string {
+ if (this.overridenInputTypes[index]) {
+ return this.overridenInputTypes[index];
+ }
+
+ return this.options.inputs[index].type;
+ }
+
+ public getOutputRunwayDirection(): Direction2D {
+ return 'down';
+ }
+
+ private getCorrespondingInputIndex(chunk: MessageChunk): number {
+ if (chunk.type !== 'index_var') {
+ return null;
+ }
+ return (chunk.index + this.synthetic_input_count); // Skip inputs not specified by the user
+ }
+
+ private getCorrespondingOutputIndex(chunk: MessageChunk): number {
+ if (chunk.type !== 'index_var') {
+ return null;
+ }
+ return (chunk.index + this.synthetic_output_count); // Skip outputs not specified by the user
+ }
+
+ public render(canvas: SVGElement, initOpts: FlowBlockInitOpts): SVGElement {
+ this.canvas = canvas;
+ this._workspace = initOpts.workspace;
+
+ if (initOpts.position) {
+ this.position = { x: initOpts.position.x, y: initOpts.position.y };
+ }
+ else {
+ if (this.options.inputs && this.options.inputs.length > 0) {
+ this.position = {x: 0, y: INPUT_PORT_REAL_SIZE};
+ }
+ else {
+ this.position = {x: 0, y: 0};
+ }
+ }
+
+ if (this.group) { return this.group }
+
+ const y_padding = 5; // px
+ const input_initial_x_position = 10; // px
+
+ const output_initial_x_position = 10; // px
+ const outputs_x_margin = 10; // px
+ const output_plating_x_margin = 3; // px
+
+ this.group = document.createElementNS(SvgNS, 'g');
+ this.node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.rectShadow = document.createElementNS(SvgNS, 'rect');
+ this.chunkGroup = document.createElementNS(SvgNS, 'g');
+
+ this.group.setAttribute('class', 'flow_node atomic_node');
+
+ this.node.appendChild(this.rectShadow);
+ this.node.appendChild(this.rect);
+ this.node.appendChild(this.chunkGroup);
+ this.group.appendChild(this.node);
+ this.canvas.appendChild(this.group);
+
+ if (this.options.icon) {
+ this.icon = document.createElementNS(SvgNS, 'image');
+ this.icon.setAttributeNS(null, 'class', 'node_icon');
+ this.icon.setAttributeNS(null, 'href', this.options.icon);
+ this.icon.setAttributeNS(null, 'width', '4ex');
+ this.icon.setAttributeNS(null, 'height', '4ex');
+ this.icon.setAttributeNS(null, 'x', ICON_PADDING);
+
+ this.iconPlate = document.createElementNS(SvgNS, 'rect');
+ this.iconPlate.setAttributeNS(null, 'class', 'node_icon_plate');
+ this.iconPlate.setAttributeNS(null, 'x', '1.5');
+ this.iconPlate.setAttributeNS(null, 'y', '1.5');
+
+ this.iconSeparator = document.createElementNS(SvgNS, 'path');
+ this.iconSeparator.setAttributeNS(null, 'class', 'node_icon_separator');
+
+ this.group.appendChild(this.iconPlate);
+ this.group.appendChild(this.iconSeparator);
+ this.group.appendChild(this.icon);
+ }
+
+ // Calculate text correction
+ const refText = document.createElementNS(SvgNS, 'text');
+ refText.setAttribute('class', 'node_name');
+ refText.setAttributeNS(null,'textlength', '100%');
+
+ refText.setAttributeNS(null, 'x', "0");
+ refText.setAttributeNS(null, 'y', "0");
+ refText.textContent = "test";
+ this.canvas.appendChild(refText);
+
+ const refBox = refText.getBoundingClientRect();
+ this.canvas.removeChild(refText);
+ // End of text correction calculation
+
+ const box_height = (refBox.height * 3 + y_padding * 2);
+
+ // Add inputs
+ this.input_x_position = input_initial_x_position;
+ this.output_x_position = output_initial_x_position;
+
+ if (this.icon) {
+ const icon_rect = this.icon.getBBox();
+ this.input_x_position += icon_rect.width;
+ this.output_x_position += icon_rect.width;
+ }
+
+ let input_index = -1;
+
+ for (const input of this.options.inputs) {
+ input_index++;
+
+ this.addInput(input, input_index);
+ }
+
+ // Add outputs
+ let output_index = -1;
+
+ for (const output of this.options.outputs) {
+ output_index++;
+
+ const out_group = document.createElementNS(SvgNS, 'g');
+ out_group.classList.add('output');
+ this.group.appendChild(out_group);
+
+ const port_external = document.createElementNS(SvgNS, 'circle');
+ const port_internal = document.createElementNS(SvgNS, 'circle');
+
+ out_group.appendChild(port_external);
+ out_group.appendChild(port_internal);
+
+ const output_port_size = 50;
+ const output_port_internal_size = 5;
+ const output_position_start = this.output_x_position;
+ let output_position_end = this.output_x_position + output_port_size;
+
+
+ if (output.name) {
+ // Bind output name and port
+ const port_plating = document.createElementNS(SvgNS, 'rect');
+ out_group.appendChild(port_plating);
+
+ const text = document.createElementNS(SvgNS, 'text');
+ text.textContent = output.name;
+ text.setAttributeNS(null, 'class', 'argument_name output');
+ out_group.appendChild(text);
+
+ output_position_end = Math.max(output_position_end, (this.output_x_position
+ + text.getBoundingClientRect().width
+ + output_plating_x_margin * 2));
+
+ const output_width = output_position_end - output_position_start;
+
+ text.setAttributeNS(null, 'x', output_position_start + output_width/2 - text.getBoundingClientRect().width/2 + '');
+ text.setAttributeNS(null, 'y', box_height - (OUTPUT_PORT_REAL_SIZE/2) + '' );
+
+ this.output_x_position = output_position_end + outputs_x_margin;
+
+ const output_height = Math.max(output_port_size / 2, (OUTPUT_PORT_REAL_SIZE
+ + text.getBoundingClientRect().height));
+
+ // Configure port connector now that we know where the output will be positioned
+ port_plating.setAttributeNS(null, 'class', 'port_plating');
+ port_plating.setAttributeNS(null, 'x', output_position_start + '');
+ port_plating.setAttributeNS(null, 'y', box_height - output_height/1.5 + '');
+ port_plating.setAttributeNS(null, 'width', (output_position_end - output_position_start) + '');
+ port_plating.setAttributeNS(null, 'height', output_height/1.5 + '');
+
+ }
+ else {
+ this.output_x_position += output_port_size;
+ }
+
+ let type_class = 'unknown_type';
+ if (output.type) {
+ type_class = output.type + '_port';
+ }
+
+ // Draw the output port
+ const port_x_center = (output_position_start + output_position_end) / 2;
+ const port_y_center = box_height;
+
+ port_external.setAttributeNS(null, 'class', 'output external_port ' + type_class);
+ port_external.setAttributeNS(null, 'cx', port_x_center + '');
+ port_external.setAttributeNS(null, 'cy', port_y_center + '');
+ port_external.setAttributeNS(null, 'r', OUTPUT_PORT_REAL_SIZE + '');
+
+ port_internal.setAttributeNS(null, 'class', 'output internal_port');
+ port_internal.setAttributeNS(null, 'cx', port_x_center + '');
+ port_internal.setAttributeNS(null, 'cy', port_y_center + '');
+ port_internal.setAttributeNS(null, 'r', output_port_internal_size + '');
+
+ if (this.options.on_io_selected) {
+ const element_index = output_index; // Capture for use in callback
+ out_group.onclick = ((_ev: MouseEvent) => {
+ this.options.on_io_selected(this, 'out', element_index, output,
+ { x: port_x_center, y: port_y_center });
+ });
+ }
+ this.output_groups[output_index] = out_group;
+ }
+
+ // Draw chunks
+ for (const chunk of this.chunks) {
+ if (chunk.type === 'const') {
+ const text = document.createElementNS(SvgNS, 'text');
+ this.chunkGroup.appendChild(text);
+
+ text.setAttribute('class', 'node_name');
+ text.setAttributeNS(null,'textlength', '100%');
+ text.setAttributeNS(null, 'y', box_height/1.75 + "");
+ text.textContent = chunk.val;
+
+ this.chunkBoxes.push(text);
+ }
+ else if (chunk.type === 'named_var') {
+ const group = document.createElementNS(SvgNS, 'g');
+ group.setAttributeNS(null, 'class', 'named_var');
+ this.chunkGroup.appendChild(group);
+
+ const plate = document.createElementNS(SvgNS, 'rect');
+ const text = document.createElementNS(SvgNS, 'text');
+ const image = document.createElementNS(SvgNS, 'image');
+
+ group.appendChild(plate);
+ group.appendChild(text);
+ group.appendChild(image);
+
+ text.setAttribute('class', 'var_name dropdown_value');
+ text.setAttributeNS(null,'textlength', '100%');
+ text.setAttributeNS(null, 'y', box_height/1.75 + "");
+ text.textContent = chunk.val;
+
+ plate.setAttribute('class', 'var_plate');
+ plate.setAttributeNS(null, 'y', box_height/2 - refBox.height + "");
+ plate.setAttributeNS(null, 'height', refBox.height * 2 + "");
+
+ image.setAttributeNS(null, 'class', 'var_dropdown_icon');
+ image.setAttributeNS(null, 'href', '/assets/sprites/expand_more.svg');
+ image.setAttributeNS(null, 'width', '2ex');
+ image.setAttributeNS(null, 'height', '2ex');
+ image.setAttributeNS(null, 'y', box_height/2 - image.getBoundingClientRect().height/2 + "");
+
+ this.namedChunkTextBoxes[chunk.name] = text;
+
+ if (this.options.on_dropdown_extended) {
+ group.onclick = () => {
+ this.options.on_dropdown_extended(this,
+ chunk.name,
+ chunk.val,
+ plate.getBBox(),
+ (new_value: string) => {
+ this.updateChunk(chunk, new_value);
+ this.onOptionsUpdate();
+ }
+ );
+ };
+ }
+
+ this.chunkBoxes.push(group);
+ }
+ else if (chunk.type === 'index_var') {
+ const group = document.createElementNS(SvgNS, 'g');
+ group.setAttributeNS(null, 'class', 'index_var');
+ this.chunkGroup.appendChild(group);
+
+ const connector = document.createElementNS(SvgNS, 'rect');
+ const path = document.createElementNS(SvgNS, 'path');
+
+ group.appendChild(connector);
+ group.appendChild(path);
+
+ let type = 'any';
+ if (chunk.direction === 'in'){
+ const inp = this.options.inputs[this.getCorrespondingInputIndex(chunk)];
+ if (!inp) {
+ console.error(chunk, this.options.inputs, this.chunks);
+ }
+ else {
+ type = inp.type || type;
+ }
+ }
+ else if (chunk.direction === 'out'){
+ const outp = this.options.outputs[this.getCorrespondingOutputIndex(chunk)];
+ if (!outp) {
+ console.error(chunk, this.options.outputs, this.chunks);
+ }
+ else {
+ type = outp.type || type;
+ }
+ }
+
+ connector.setAttribute('class', `var_connector direction_${chunk.direction} ${type}_port`);
+ connector.setAttributeNS(null, 'y', box_height/2 - CONNECTOR_SIDE_SIZE / 2 + "");
+ connector.setAttributeNS(null, 'height', CONNECTOR_SIDE_SIZE + "");
+
+ path.setAttribute('class', `var_path direction_${chunk.direction} ${type}_port`);
+
+ this.chunkBoxes.push(group);
+ }
+ }
+
+ // Properly place elements
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+ this.rect.setAttributeNS(null, 'rx', "2px"); // Like border-radius, in px
+
+ this.rectShadow.setAttributeNS(null, 'class', "body_shadow");
+ this.rectShadow.setAttributeNS(null, 'x', "0");
+ this.rectShadow.setAttributeNS(null, 'y', "0");
+ this.rectShadow.setAttributeNS(null, 'height', box_height + "");
+ this.rectShadow.setAttributeNS(null, 'rx', "2px"); // Like border-radius, in px
+
+ if (this.icon) {
+ const icon_rect = this.icon.getBBox();
+ this.icon.setAttributeNS(null, 'y', box_height / 2 - icon_rect.height / 2 + '');
+ const padding_px = icon_rect.x;
+
+ const separator_x = icon_rect.width + padding_px * 2;
+ this.iconSeparator.setAttributeNS(null, 'd', `M${ separator_x },0 L${separator_x},${box_height}`);
+
+ this.iconPlate.setAttributeNS(null, 'width', separator_x + 1 + ''); // +1 to cover separator stroke-width
+ this.iconPlate.setAttributeNS(null, 'height', box_height - 3 + '');
+
+ }
+
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+
+ this.updateBody();
+
+ return this.group;
+ }
+
+}
diff --git a/frontend/src/app/flow-editor/base_toolbox_description.ts b/frontend/src/app/flow-editor/base_toolbox_description.ts
new file mode 100644
index 00000000..8df5b197
--- /dev/null
+++ b/frontend/src/app/flow-editor/base_toolbox_description.ts
@@ -0,0 +1,916 @@
+import { AtomicFlowBlockOptions } from './atomic_flow_block';
+import { PLATFORM_ICON } from './definitions';
+import { UiFlowBlockOptions } from './ui-blocks/ui_flow_block';
+import { ContainerFlowBlockOptions } from './ui-blocks/container_flow_block';
+
+interface Category {
+ id: string,
+ name: string,
+ blocks: ((AtomicFlowBlockOptions | UiFlowBlockOptions | ContainerFlowBlockOptions) & { is_internal?: boolean })[],
+}
+
+export type ToolboxDescription = Category[];
+
+export const OP_PRELOAD_BLOCK: AtomicFlowBlockOptions = {
+ icon: PLATFORM_ICON,
+ message: 'Preload getter',
+ block_function: 'op_preload_getter',
+ type: 'operation',
+ inputs: [
+ {
+ name: "getter",
+ type: "any",
+ },
+ ],
+ outputs: [],
+};
+
+export const OP_ON_BLOCK_RUN: AtomicFlowBlockOptions = {
+ icon: PLATFORM_ICON,
+ message: 'On block run',
+ block_function: 'op_on_block_run',
+ type: 'trigger',
+ inputs: [
+ {
+ name: "block_id",
+ type: "string",
+ },
+ {
+ name: "block_id",
+ type: "integer",
+ },
+ ],
+ outputs: [],
+};
+
+export const ADVANCED_CATEGORY = 'advanced';
+export const INTERNAL_CATEGORY = '__internal__';
+
+export const BaseToolboxDescription: ToolboxDescription = [
+ {
+ id: 'control',
+ name: 'Control',
+ blocks: [
+ {
+ icon: PLATFORM_ICON,
+ message: 'Wait',
+ block_function: 'control_wait',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ name: "seconds to wait",
+ type: "integer",
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Broadcast to all users',
+ block_function: 'control_broadcast_to_all_users',
+ type: 'operation',
+ fixed_pulses: true,
+ inputs: [
+ {
+ required: true,
+ type: "user-pulse",
+ },
+ ],
+ outputs: [
+ {
+ type: "pulse",
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Check',
+ block_function: 'control_if_else',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ name: "check",
+ type: "boolean",
+ },
+ ],
+ outputs: [
+ {
+ name: 'if true',
+ type: 'pulse',
+ },
+ {
+ name: 'if false',
+ type: 'pulse',
+ }
+ ],
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Wait for %i1 to be true',
+ block_function: 'control_wait_until',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ name: "check",
+ type: "boolean",
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'On new %i1 value',
+ block_function: 'trigger_on_signal',
+ type: 'trigger',
+ inputs: [
+ {
+ required: true,
+ name: "signal",
+ type: "any",
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Wait for pulse %i1 before passing signal %i2',
+ block_function: 'control_signal_wait_for_pulse',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ name: "pulse",
+ type: "pulse",
+ },
+ {
+ required: true,
+ name: "signal",
+ type: "any",
+ },
+ ],
+ outputs: [
+ {
+ type: "any",
+ }
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Wait for next value',
+ block_function: 'control_wait_for_next_value',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ type: "any",
+ },
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "any",
+ }
+ ],
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Repeat times',
+ block_function: 'control_repeat',
+ type: 'operation',
+ inputs: [
+ {
+ name: "start loop",
+ type: "pulse",
+ },
+ {
+ required: true,
+ name: "repetition times",
+ type: "integer",
+ },
+ ],
+ outputs: [
+ {
+ name: "loop continues",
+ type: "pulse",
+ },
+ {
+ name: "iteration #",
+ type: "integer",
+ },
+ {
+ name: "loop completed",
+ type: "pulse",
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'When all true',
+ block_function: 'trigger_when_all_true',
+ type: 'trigger',
+ inputs: [
+ {
+ type: "boolean",
+ },
+ {
+ type: "boolean",
+ },
+ ],
+ extra_inputs: {
+ type: "boolean",
+ quantity: "any",
+ },
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Run on parallel',
+ block_function: 'op_fork_execution',
+ type: 'operation',
+ outputs: [
+ {
+ type: "pulse",
+ },
+ {
+ type: "pulse",
+ },
+ {
+ type: "pulse",
+ },
+ {
+ type: "pulse",
+ },
+ {
+ type: "pulse",
+ },
+ {
+ type: "pulse",
+ },
+ ],
+ // TODO: Implement extra_outputs
+ // extra_outputs: {
+ // type: "boolean",
+ // quantity: "any",
+ // },
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'When all completed',
+ block_function: 'trigger_when_all_completed',
+ type: 'trigger',
+ inputs: [
+ {
+ type: "pulse",
+ },
+ {
+ type: "pulse",
+ },
+ ],
+ extra_inputs: {
+ type: "pulse",
+ quantity: "any",
+ },
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'When first completed',
+ block_function: 'trigger_when_first_completed',
+ type: 'trigger',
+ inputs: [
+ {
+ type: "pulse",
+ },
+ {
+ type: "pulse",
+ },
+ ],
+ extra_inputs: {
+ type: "pulse",
+ quantity: "any",
+ },
+ }
+ ]
+ },
+ {
+ id: 'operators',
+ name: 'Operators',
+ blocks: [
+ {
+ icon: PLATFORM_ICON,
+ message: '%i1 + %i2',
+ block_function: 'operator_add',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: "float",
+ },
+ {
+ required: true,
+ type: "float",
+ },
+ ],
+ outputs: [
+ {
+ type: 'float',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: '%i1 - %i2',
+ block_function: 'operator_subtract',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: "float",
+ },
+ {
+ required: true,
+ type: "float",
+ },
+ ],
+ outputs: [
+ {
+ type: 'float',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: '%i1 × %i2',
+ block_function: 'operator_multiply',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: "float",
+ },
+ {
+ required: true,
+ type: "float",
+ },
+ ],
+ outputs: [
+ {
+ type: 'float',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: '%i1 / %i2',
+ block_function: 'operator_divide',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ name: 'dividend',
+ type: "float",
+ },
+ {
+ required: true,
+ name: 'divisor',
+ type: "float",
+ },
+ ],
+ outputs: [
+ {
+ type: 'float',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: '%i1 modulo %i2',
+ block_function: 'operator_modulo',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ name: 'dividend',
+ type: "float",
+ },
+ {
+ required: true,
+ name: 'divisor',
+ type: "float",
+ },
+ ],
+ outputs: [
+ {
+ type: 'float',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Is %i1 greater (>) than %i2 ?',
+ block_function: 'operator_gt',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ name: "bigger",
+ type: "float",
+ },
+ {
+ required: true,
+ name: "smaller",
+ type: "float",
+ },
+ ],
+ outputs: [
+ {
+ type: 'boolean',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Are all equals?',
+ block_function: 'operator_equals',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: "any",
+ },
+ {
+ required: true,
+ type: "any",
+ },
+ ],
+ extra_inputs: {
+ type: "any",
+ quantity: "any",
+ },
+ outputs: [
+ {
+ type: 'boolean',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Is %i1 less (<) than %i2?',
+ block_function: 'operator_lt',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ name: "smaller",
+ type: "float",
+ },
+ {
+ required: true,
+ name: "bigger",
+ type: "float",
+ },
+ ],
+ outputs: [
+ {
+ type: 'boolean',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'All true',
+ block_function: 'operator_and',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: "boolean",
+ },
+ {
+ required: true,
+ type: "boolean",
+ },
+ ],
+ outputs: [
+ {
+ type: 'boolean',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Any true',
+ block_function: 'operator_or',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: "boolean",
+ },
+ {
+ required: true,
+ type: "boolean",
+ },
+ ],
+ outputs: [
+ {
+ type: 'boolean',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Inverse',
+ block_function: 'operator_not',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: "boolean",
+ },
+ ],
+ outputs: [
+ {
+ type: 'boolean',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Join texts',
+ block_function: 'operator_join',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ name: "beginning",
+ type: "string",
+ },
+ {
+ required: true,
+ name: "end",
+ type: "string",
+ },
+ ],
+ outputs: [
+ {
+ type: 'string',
+ },
+ ]
+ },
+ // Advanced block
+ {
+ icon: PLATFORM_ICON,
+ message: 'Get key %i1 of %i2',
+ block_function: 'operator_json_parser',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ name: "key",
+ type: "string",
+ },
+ {
+ required: true,
+ name: "dictionary",
+ type: "any",
+ },
+ ],
+ outputs: [
+ {
+ type: 'any',
+ },
+ ]
+ },
+ ]
+ },
+ {
+ id: 'debug',
+ name: 'Debug',
+ blocks: [
+ {
+ icon: PLATFORM_ICON,
+ message: 'Log value %i1',
+ block_function: 'logging_add_log',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ type: "any",
+ },
+ ],
+ },
+ ]
+ },
+ {
+ id: 'time',
+ name: 'Time',
+ blocks: [
+ {
+ icon: PLATFORM_ICON,
+ message: 'UTC date',
+ block_function: 'flow_utc_date',
+ type: 'getter',
+ outputs: [
+ {
+ name: 'year',
+ type: 'integer',
+ },
+ {
+ name: 'month',
+ type: 'integer',
+ },
+ {
+ name: 'day',
+ type: 'integer',
+ },
+ {
+ name: 'day of week',
+ type: 'any',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'UTC time',
+ block_function: 'flow_utc_time',
+ type: 'getter',
+ outputs: [
+ {
+ name: 'hour',
+ type: 'integer',
+ },
+ {
+ name: 'minute',
+ type: 'integer',
+ },
+ {
+ name: 'second',
+ type: 'integer',
+ },
+ ]
+ }
+ ]
+ },
+ {
+ id: 'variables',
+ name: 'Variables',
+ blocks: [
+ {
+ icon: PLATFORM_ICON,
+ message: 'Get %(variable) value',
+ block_function: 'data_variable',
+ type: 'getter',
+ outputs: [
+ {
+ type: 'any'
+ }
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Set %(variable) to %i1',
+ block_function: 'data_setvariableto',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ name: 'new value',
+ type: "any",
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Increment %(variable) by %i1',
+ block_function: 'data_changevariableby',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ type: "float",
+ },
+ ]
+ }
+ ]
+ },
+ {
+ id: 'lists',
+ name: 'Lists',
+ blocks: [
+ {
+ icon: PLATFORM_ICON,
+ message: 'Set list %(list) to %i1',
+ block_function: 'data_setlistto',
+ inputs: [
+ {
+ required: true,
+ type: 'list',
+ }
+ ],
+ type: 'operation'
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Add %i1 to %(list)',
+ block_function: 'data_addtolist',
+ inputs: [
+ {
+ required: true,
+ type: 'any',
+ }
+ ],
+ type: 'operation'
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Delete entry # %i1 to %(list)',
+ block_function: 'data_deleteoflist',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ type: 'integer',
+ }
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Delete all of %(list)',
+ block_function: 'data_deletealloflist',
+ type: 'operation'
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Insert %i1 at position %i2 of %(list)',
+ block_function: 'data_insertatlist',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ type: 'any',
+ },
+ {
+ required: true,
+ type: 'integer',
+ }
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Replace item at position %i1 of %(list) with %i2',
+ block_function: 'data_replaceitemoflist',
+ type: 'operation',
+ inputs: [
+ {
+ required: true,
+ type: 'integer',
+ },
+ {
+ required: true,
+ type: 'any',
+ }
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Item number %i1 of %(list)',
+ block_function: 'data_itemoflist',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: 'integer',
+ },
+ ],
+ outputs: [
+ {
+ type: 'any',
+ }
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Position of item %i1 in %(list)',
+ block_function: 'data_itemnumoflist',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: 'any',
+ },
+ ],
+ outputs: [
+ {
+ type: 'integer',
+ },
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Number of items in %(list)',
+ block_function: 'data_lengthoflist',
+ type: 'getter',
+ outputs: [
+ {
+ type: 'integer',
+ }
+ ]
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'Does %(list) contain %i1?',
+ block_function: 'data_listcontainsitem',
+ type: 'getter',
+ inputs: [
+ {
+ required: true,
+ type: 'any',
+ }
+ ],
+ outputs: [
+ {
+ type: 'boolean',
+ }
+ ]
+ },
+ ]
+ },
+ {
+ id: ADVANCED_CATEGORY,
+ name: 'Advanced',
+ blocks: [
+ {
+ icon: PLATFORM_ICON,
+ message: 'Get thread ID',
+ block_function: 'flow_get_thread_id',
+ type: 'getter',
+ outputs: [
+ {
+ type: "string",
+ },
+ ],
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'When bridge %i1 connects',
+ block_function: 'trigger_on_bridge_connected',
+ type: 'trigger',
+ inputs: [
+ {
+ required: true,
+ name: "bridge",
+
+ enum_name: "bridges",
+ enum_namespace: "programaker",
+ type: "enum",
+ },
+ ],
+ outputs: [],
+ },
+ {
+ icon: PLATFORM_ICON,
+ message: 'When bridge %i1 connection STOPS',
+ block_function: 'trigger_on_bridge_disconnected',
+ type: 'trigger',
+ inputs: [
+ {
+ required: true,
+ name: "bridge",
+
+ enum_name: "bridges",
+ enum_namespace: "programaker",
+ type: "enum",
+ },
+ ],
+ outputs: [],
+ },
+ ]
+ },
+ {
+ id: INTERNAL_CATEGORY,
+ name: 'Internal blocks', // Not to be placed manually!
+ blocks: [
+ OP_PRELOAD_BLOCK,
+ OP_ON_BLOCK_RUN,
+ ]
+ }
+];
+
+const BLOCK_MAP: {[key: string]: AtomicFlowBlockOptions} = {};
+let BLOCK_MAP_READY = false;
+export function get_block_from_base_toolbox(func_name: string): AtomicFlowBlockOptions {
+ if (!BLOCK_MAP_READY) {
+ build_block_map();
+ }
+ return BLOCK_MAP[func_name];
+}
+
+function build_block_map() {
+ for (const cat of BaseToolboxDescription) {
+ for (const block of cat.blocks) {
+ let ablock = block as AtomicFlowBlockOptions;
+ if (ablock.block_function) {
+ BLOCK_MAP[ablock.block_function] = ablock;
+ }
+ }
+ }
+}
diff --git a/frontend/src/app/flow-editor/block_exhibitor.ts b/frontend/src/app/flow-editor/block_exhibitor.ts
new file mode 100644
index 00000000..e483fd40
--- /dev/null
+++ b/frontend/src/app/flow-editor/block_exhibitor.ts
@@ -0,0 +1,88 @@
+import { BlockManager } from './block_manager';
+import { EnumValue } from './enum_direct_value';
+import { Area2D, FlowBlock, InputPortDefinition, OutputPortDefinition, MessageType } from './flow_block';
+import { DirectValue } from './direct_value';
+import { uuidv4 } from './utils';
+
+const SvgNS = "http://www.w3.org/2000/svg";
+export type BlockGenerator = (manager: BlockManager, blockId: string) => FlowBlock;
+
+export class BlockExhibitor implements BlockManager {
+ private baseElement: HTMLElement;
+
+ private element: SVGSVGElement;
+ private block: FlowBlock;
+
+ public static FromGenerator(generator: BlockGenerator, baseElement: HTMLElement) {
+ const ex = new BlockExhibitor(baseElement);
+ ex.init(generator);
+ return ex;
+ }
+
+ private constructor(baseElement: HTMLElement) {
+ this.baseElement = baseElement;
+ }
+
+ // Block manager interface
+ onIoSelected(_block: FlowBlock,
+ _type: 'in'|'out',
+ _index: number,
+ _definition: InputPortDefinition | OutputPortDefinition,
+ _port_center: {x: number, y: number},
+ ): void {
+ // Do nothing
+ }
+
+ onInputsChanged(_block: FlowBlock,
+ _input_num: number,
+ ): void {
+ // Do nothing
+ }
+
+ onSelectRequested(_block: FlowBlock,
+ _previous_value: string,
+ _values: EnumValue[],
+ _value_dict: {[key:string]: EnumValue},
+ _update: (new_value: string) => void) : void {
+ // Do nothing
+ }
+
+ onRequestEdit(_block: DirectValue, _type: MessageType, _update: (value: string) => void): void {
+ // Do nothing
+ }
+
+ onDropdownExtended(_block: FlowBlock,
+ _slot_id: string,
+ _previous_value: string,
+ _current_rect: Area2D,
+ _update: (new_value: string) => void,
+ ): void {
+ console.warn('Dropdown extension not implemented on block exhibitor')
+ }
+
+ // Block exhibitor management
+ private init(generator: BlockGenerator) {
+ this.element = document.createElementNS(SvgNS, 'svg');
+ this.element.setAttribute('class', 'block_renderer block_exhibitor');
+ this.baseElement.appendChild(this.element);
+
+ this.block = generator(this, uuidv4());
+ this.block.render(this.element, {});
+
+ const area = this.block.getBodyArea();
+
+ this.block.moveBy({x: 5, y: 5}); // Move slightly, to allow all of the block shadow in
+
+ // Move block into view
+ this.element.style.width = area.width + 10 + 'px';
+ this.element.style.height = area.height + 10 + 'px';
+ }
+
+ public getElement(): SVGSVGElement {
+ return this.element;
+ }
+
+ public getInnerElementRect(): Area2D {
+ return this.block.getBodyElement().getBoundingClientRect();
+ }
+}
diff --git a/frontend/src/app/flow-editor/block_manager.ts b/frontend/src/app/flow-editor/block_manager.ts
new file mode 100644
index 00000000..678765ec
--- /dev/null
+++ b/frontend/src/app/flow-editor/block_manager.ts
@@ -0,0 +1,11 @@
+import { OnRequestEdit } from './direct_value';
+import { OnSelectRequested } from './enum_direct_value';
+import { OnDropdownExtended, OnInputsChanged, OnIOSelected } from './flow_block';
+
+export interface BlockManager {
+ onIoSelected: OnIOSelected;
+ onInputsChanged: OnInputsChanged;
+ onDropdownExtended: OnDropdownExtended;
+ onRequestEdit: OnRequestEdit;
+ onSelectRequested: OnSelectRequested;
+}
diff --git a/frontend/src/app/flow-editor/definitions.ts b/frontend/src/app/flow-editor/definitions.ts
new file mode 100644
index 00000000..afa33712
--- /dev/null
+++ b/frontend/src/app/flow-editor/definitions.ts
@@ -0,0 +1,6 @@
+export const BASE_TOOLBOX_SOURCE_SIGNALS = [
+ 'flow_utc_time',
+];
+
+export const PLATFORM_ICON = '/assets/logo-dark.png';
+export const UI_ICON = '/assets/logo-dark.png';
diff --git a/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.html b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.html
new file mode 100644
index 00000000..5b3f1b49
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.html
@@ -0,0 +1,146 @@
+
+ settings
+ Block settings
+
+
+
+
+
Background configuration
+
+
+ Color
+
+ Transparent
+
+
+
+
+
+
+
+
+
+
Text configuration
+
+
+
+
+
+
+
+ Boldness:
+
+
+ SuperLight
+ Light
+ Normal
+ Bold
+ SuperBold
+
+
+
+
+
Result sample:
+
+ The quick brown fox jumps over the lazy dog
+
+
+
+
+
+
+
Element configuration
+
+
+
+
+
+
+ edit Upload {{ bodyCurrentImage ? ' a new ' : '' }} picture
+
+
+
+
+
+
+ Element width
+
+
+ {{ width }}
+
+
+
+
+
+
+
+
+
Element target
+
+
+
+
+ Link Target
+
+ http://... or mailto:some@mail.com
+ {{getUrlErrorMessage()}}
+
+
+
+
+
+
+
+
+
+ clear
+ Cancel
+
+
+
+
+ save
+ Accept changes
+
+
+
+
diff --git a/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.scss b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.scss
new file mode 100644
index 00000000..9a1f70cb
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.scss
@@ -0,0 +1,104 @@
+h2 mat-icon {
+ vertical-align: sub;
+ margin-right: 1ex;
+}
+
+.block-settings {
+ h3 {
+ font-size: 1.25rem;
+ }
+
+ .section-contents {
+ margin: 0 0 1em 1em;
+ }
+
+ mat-radio-group mat-radio-button {
+ margin-right: 1em;
+ }
+
+ .color-picker {
+ margin-top: 1em;
+
+ input {
+ // Mostly the same style as the one on https://huebee.buzz/
+ box-shadow: inset 0 2px 10px hsla(0, 0%, 0%, 0.15);
+ border-radius: 5px;
+ border: 1px solid hsla(0, 0%, 0%, 0.2);
+
+ padding: 0.85ex;
+ font-size: 1.2rem;
+ width: 23ex;
+
+ margin-bottom: 1ex;
+ }
+ }
+
+ .image-picker {
+ margin-top: 1em;
+
+ .avatar img {
+ width: 20rem;
+ height: 20rem;
+ }
+
+ .picture-upload-button {
+ display: block;
+ margin: 1ex auto;
+ }
+ }
+
+ .target-link-picker mat-form-field {
+ width: 50ex;
+ max-width: 90vw;
+ }
+
+ .extra-options {
+ margin-left: 1em;
+ margin-top: 1em;
+ padding: 0;
+ }
+}
+
+.width-sample .sample-container {
+ height: 2rem;
+ border: 1px dashed #000;
+
+ .result {
+ height: 100%;
+ background-color: #27212e;
+ }
+}
+
+.text-sample {
+ padding-top: 1em;
+
+ .result {
+ padding: 1rem;
+ border: 1px dashed #000;
+
+ width: 40rem;
+ max-width: 90vw;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-height: 8rem;
+ line-height: 2ex;
+ }
+}
+
+.accept-cancel {
+ margin-top: 2em;
+}
+
+.accept-cancel button {
+ margin-right: 1ex;
+}
+
+.confirm-button {
+ background-color: #009688;
+ color: white;
+ font-weight: bold;
+}
+
+.hidden {
+ display: none;
+}
diff --git a/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.spec.ts b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.spec.ts
new file mode 100644
index 00000000..301986c9
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.spec.ts
@@ -0,0 +1,60 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { CookiesService } from '@ngx-utils/cookies';
+import { BrowserCookiesModule, BrowserCookiesService } from '@ngx-utils/cookies/browser';
+import { ConfigureBlockDialogComponent } from './configure-block-dialog.component';
+import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+
+
+describe('ConfigureBlockDialogComponent', () => {
+ let component: ConfigureBlockDialogComponent;
+ let fixture: ComponentFixture;
+
+ const mockDialogRef = {
+ close: jasmine.createSpy('close'),
+ };
+ const mockDialogData = {
+ block: {
+ getAllowedConfigurations: () => { return {}; },
+ getCurrentConfiguration: () => { return {}; },
+ },
+ programId: 'test-id',
+ };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ BrowserCookiesModule.forRoot(),
+ RouterTestingModule,
+ HttpClientTestingModule,
+ MatDialogModule,
+ ],
+ declarations: [ ConfigureBlockDialogComponent ],
+ providers: [
+ {
+ provide: CookiesService,
+ useClass: BrowserCookiesService,
+ },
+ {
+ provide: MatDialogRef,
+ useValue: mockDialogRef
+ },
+ {
+ provide: MAT_DIALOG_DATA,
+ useValue: mockDialogData,
+ }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureBlockDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.ts b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.ts
new file mode 100644
index 00000000..c43b4057
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-block-dialog/configure-block-dialog.component.ts
@@ -0,0 +1,391 @@
+import { AfterViewInit, Component, ElementRef, Inject, ViewChild } from '@angular/core';
+import { MatButton } from '@angular/material/button';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatRadioChange, MatRadioGroup } from '@angular/material/radio';
+import { SessionService } from '../../../session.service';
+import { BackgroundPropertyConfiguration } from '../../ui-blocks/renderers/ui_tree_repr';
+import { UrlPattern } from '../configure-link-dialog/configure-link-dialog.component';
+import { Validators, FormControl } from '@angular/forms';
+
+declare const Huebee: any;
+
+export type FontWeight = 'super-light' | 'light' | 'normal' | 'bold' | 'super-bold';
+
+export type ColorOption = { color: boolean };
+export type ImageOption = { image?: boolean };
+export type FontSizeOption = { fontSize: boolean };
+export type FontWeightOption = { fontWeight?: boolean };
+export type ImageAssetConfiguration = {
+ id: string,
+};
+export type WidthTakenOption = {widthTaken?: {name: string, style: string}[]};
+type LinkOption = { link: boolean };
+
+export type SurfaceOption = ColorOption & ImageOption;
+export type TextOptions = ColorOption & FontSizeOption & FontWeightOption;
+export type BodyOptions = ImageOption & WidthTakenOption;
+export type TargetOptions = LinkOption;
+
+export type TextPropertyConfiguration = {
+ color?: { value: string },
+ fontSize?: { value: number },
+ fontWeight?: { value: FontWeight },
+};
+
+
+export type BodyPropertyConfiguration = {
+ image?: ImageAssetConfiguration,
+ widthTaken?: { value: string },
+}
+
+type TargetPropertyConfiguration = {
+ link?: { value: string };
+ openInTab?: { value: boolean };
+};
+
+export type BlockConfigurationOptions = {
+ bg?: BackgroundPropertyConfiguration,
+ text?: TextPropertyConfiguration,
+ body?: BodyPropertyConfiguration,
+ target?: TargetPropertyConfiguration,
+};
+export interface BlockAllowedConfigurations {
+ background?: SurfaceOption,
+ text?: TextOptions,
+ body?: BodyOptions,
+ target?: TargetOptions,
+};
+
+export interface ConfigurableBlock {
+ applyConfiguration(settings: BlockConfigurationOptions): void;
+ getCurrentConfiguration(): BlockConfigurationOptions;
+ getAllowedConfigurations(): BlockAllowedConfigurations,
+}
+
+export function fontWeightToCss(value: FontWeight) {
+ switch(value) {
+ case 'normal':
+ case 'bold':
+ return value;
+
+ case 'light':
+ return '300';
+
+ case 'super-light':
+ return '100';
+
+ case 'super-bold':
+ return '900';
+
+ default:
+ throw Error("Unknown boldness value: " + value);
+ }
+}
+
+
+const DEFAULT_BACKGROUND_COLOR = '#FFFFFF';
+const DEFAULT_TEXT_COLOR = '#000000';
+const DEFAULT_TEXT_SIZE = 14;
+
+@Component({
+ selector: 'app-configure-block-dialog',
+ templateUrl: './configure-block-dialog.component.html',
+ providers: [ SessionService ],
+ styleUrls: [
+ './configure-block-dialog.component.scss',
+ '../../../libs/css/material-icons.css',
+ ],
+})
+export class ConfigureBlockDialogComponent implements AfterViewInit {
+ loadedImage: File = null;
+
+ // General
+ selectedBackgroundType: 'color' | 'image' | 'transparent' = 'transparent';
+ @ViewChild('acceptSaveConfigButton') acceptSaveConfigButton: MatButton;
+
+ // Background
+ @ViewChild('bgColorPicker') bgColorPicker: ElementRef;
+ @ViewChild('bgTypeSelector') bgTypeSelector: MatRadioGroup;
+ private hbBgColorPicker: any;
+
+ // Text color/size/bold
+ @ViewChild('textColorPicker') textColorPicker: ElementRef;
+ @ViewChild('textSampleResult') textSampleResult: ElementRef;
+ @ViewChild('fontSizeValueViewer') fontSizeValueViewer: ElementRef;
+ @ViewChild('textFontSizePicker') textFontSizePicker: ElementRef;
+ fontWeightTaken: FontWeight = 'normal';
+ private hbTextColorPicker: any;
+
+ // Body image/width
+ @ViewChild('bodyImgPreview') bodyImgPreview: ElementRef;
+ @ViewChild('bodyImgFileInput') bodyImgFileInput: ElementRef;
+ bodyCurrentImage: string;
+ @ViewChild('widthSampleResult') widthSampleResult: ElementRef;
+ allowedWidthTypes: string[];
+ widthTaken: string;
+
+ // Target link
+ targetLinkControl : FormControl | null = null;
+ openInTab: boolean = false;
+
+ currentConfig: BlockConfigurationOptions;
+ allowedConfigurations: BlockAllowedConfigurations;
+
+ // Initialization
+ constructor(public dialogRef: MatDialogRef,
+ private sessionService: SessionService,
+ @Inject(MAT_DIALOG_DATA)
+ public data: { programId: string, block: ConfigurableBlock }) {
+
+ const config = this.allowedConfigurations = data.block.getAllowedConfigurations();
+
+ this.currentConfig = data.block.getCurrentConfiguration();
+ if (config.background) {
+ if (this.currentConfig.bg) {
+ this.selectedBackgroundType = this.currentConfig.bg.type;
+ }
+ else {
+ this.currentConfig.bg = { type: 'transparent' };
+ }
+ }
+
+ if (this.allowedConfigurations.body) {
+ if (this.allowedConfigurations.body.widthTaken) {
+ this.allowedWidthTypes = this.allowedConfigurations.body.widthTaken.map((x) => x.name );
+
+ if (this.currentConfig.body && this.currentConfig.body.widthTaken) {
+ this.widthTaken = this.currentConfig.body.widthTaken.value;
+ }
+ }
+ }
+
+ if (this.currentConfig.text) {
+ if (this.currentConfig.text.fontWeight) {
+ this.fontWeightTaken = this.currentConfig.text.fontWeight.value;
+ }
+ }
+
+ if (this.allowedConfigurations.target) {
+ if (this.allowedConfigurations.target.link) {
+ this.targetLinkControl = new FormControl('', [Validators.pattern(UrlPattern)]);
+
+ if (this.currentConfig.target && this.currentConfig.target.link) {
+ this.targetLinkControl.setValue(this.currentConfig.target.link.value);
+ }
+
+ if (this.currentConfig.target && this.currentConfig.target.openInTab) {
+ this.openInTab = this.currentConfig.target.openInTab.value;
+ }
+ }
+ }
+ }
+
+ ngAfterViewInit(): void {
+ this.generateForm();
+ }
+
+ generateForm() {
+ if (this.allowedConfigurations.background) {
+ if (this.currentConfig.bg.type === 'color') {
+ const color = this.currentConfig.bg.value || DEFAULT_BACKGROUND_COLOR;
+
+ this._initBgColorPicker(color);
+ }
+ }
+
+ if (this.allowedConfigurations.text) {
+ const textConf = this.currentConfig.text;
+ if (this.allowedConfigurations.text.color) {
+ let color = DEFAULT_TEXT_COLOR;
+ if (textConf && textConf.color) {
+ color = textConf.color.value;
+ }
+
+ this._initTextColorPicker(color);
+ }
+
+ if (this.allowedConfigurations.text.fontSize) {
+ let size = DEFAULT_TEXT_SIZE;
+
+ if (textConf && textConf.fontSize) {
+ size = textConf.fontSize.value;
+ }
+
+ this._initTextSizePicker(size);
+ }
+ }
+
+ if (this.currentConfig.body && this.currentConfig.body.widthTaken) {
+ this.onNewWidthTaken({ value: this.currentConfig.body.widthTaken.value });
+ }
+ }
+
+ onNewWidthTaken(change: { value: string }) {
+ const val = this.allowedConfigurations.body.widthTaken.find(x => x.name == change.value);
+ this.widthSampleResult.nativeElement.style.width = val.style;
+ }
+
+ onNewFontWeightTaken(change: MatRadioChange) {
+ this.textSampleResult.nativeElement.style.fontWeight = fontWeightToCss(change.value);
+ }
+
+ onNewBgType(change: MatRadioChange) {
+ if (change.value === 'color' && (!this.hbBgColorPicker)) {
+ this._initBgColorPicker(DEFAULT_BACKGROUND_COLOR);
+ }
+
+ if (change.value === 'transparent' && (this.allowedConfigurations.text)) {
+ this.textSampleResult.nativeElement.style.backgroundColor = 'transparent';
+ }
+ }
+
+ private _initBgColorPicker(startingColor: string) {
+ setTimeout(() => {
+ this.hbBgColorPicker = new Huebee(this.bgColorPicker.nativeElement, {
+ // options
+ notation: 'hex',
+ saturations: 2,
+ setBGColor: true,
+ staticOpen: true,
+ });
+ this.hbBgColorPicker.setColor(startingColor);
+
+ if (this.allowedConfigurations.text) {
+ this.textSampleResult.nativeElement.style.backgroundColor = startingColor;
+
+ this.hbBgColorPicker.on('change', (color: string, _hue: any, _sat: any, _lum: any) => {
+ this.textSampleResult.nativeElement.style.backgroundColor = color;
+ });
+ }
+ }, 0); // Update this *after* the appropriate changes had happened
+ }
+
+ private _initTextColorPicker(startingColor: string) {
+ setTimeout(() => {
+ // Color picker
+ this.hbTextColorPicker = new Huebee(this.textColorPicker.nativeElement, {
+ // options
+ notation: 'hex',
+ saturations: 2,
+ setText: true,
+ setBgColor: false,
+ staticOpen: true,
+ });
+ this.hbTextColorPicker.setColor(startingColor);
+
+ this.textSampleResult.nativeElement.style.color = startingColor;
+ this.hbTextColorPicker.on('change', (color: string, _hue: any, _sat: any, _lum: any) => {
+ this.textSampleResult.nativeElement.style.color = color;
+ });
+ }, 0); // Update this *after* the appropriate changes had happened
+ }
+
+ private _initTextSizePicker(startingSize: number) {
+ setTimeout(() => {
+ // Font size selector
+ this.textFontSizePicker.nativeElement.value = startingSize + '';
+ this.fontSizeValueViewer.nativeElement.innerText = startingSize + '';
+ this.textSampleResult.nativeElement.style.fontSize = startingSize + 'px';
+
+ this.textFontSizePicker.nativeElement.oninput = (ev) => {
+ const value = (ev.srcElement as HTMLInputElement).value;
+
+ this.fontSizeValueViewer.nativeElement.innerText = value;
+ this.textSampleResult.nativeElement.style.fontSize = value + 'px';
+ }
+ }, 0); // Update this *after* the appropriate changes had happened
+
+ }
+
+ // Operation
+ previewImage(event: KeyboardEvent) {
+ const input: HTMLInputElement = event.target as HTMLInputElement;
+
+ if (input.files && input.files[0]) {
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ this.loadedImage = input.files[0];
+ this.bodyImgPreview.nativeElement.src = e.target.result as string;
+ }
+
+ reader.readAsDataURL(input.files[0]);
+ }
+ }
+
+ getUrlErrorMessage() {
+ if (this.targetLinkControl.hasError('required')) {
+ return 'You must enter a value';
+ }
+
+ return this.targetLinkControl.hasError('pattern') ? 'Not a valid link' : '';
+ }
+
+ isValid(): boolean {
+ if (this.targetLinkControl && !(this.targetLinkControl.valid)) {
+ return false;
+ }
+ return true;
+ }
+
+ // Accept/cancel
+ cancelChanges() {
+ this.dialogRef.close({success: false});
+ }
+
+ async acceptChanges() {
+ const settings: BlockConfigurationOptions = {};
+
+ const buttonClass = this.acceptSaveConfigButton._elementRef.nativeElement.classList;
+ buttonClass.add('started');
+ buttonClass.remove('completed');
+
+ if (this.allowedConfigurations.background) {
+ if (this.selectedBackgroundType === 'color') {
+ settings.bg = { type: 'color', value: this.hbBgColorPicker.color };
+ }
+ else if (this.selectedBackgroundType === 'transparent') {
+ settings.bg = { type: 'transparent' };
+ }
+ }
+
+ if (this.allowedConfigurations.text) {
+ settings.text = {};
+ if (this.allowedConfigurations.text.color) {
+ settings.text.color = { value: this.hbTextColorPicker.color };
+ }
+ if (this.allowedConfigurations.text.fontSize) {
+ settings.text.fontSize = { value: this.textFontSizePicker.nativeElement.valueAsNumber };
+ }
+ if (this.allowedConfigurations.text.fontWeight) {
+ settings.text.fontWeight = { value: this.fontWeightTaken };
+ }
+ }
+
+ if (this.allowedConfigurations.body) {
+ settings.body = {};
+ if (this.loadedImage) {
+ const imageId = (await this.sessionService.uploadAsset(this.loadedImage, this.data.programId)).value;
+ settings.body.image = { id: imageId };
+ }
+
+ if (this.widthTaken) {
+ settings.body.widthTaken = { value: this.widthTaken };
+ }
+ }
+
+ if (this.allowedConfigurations.target) {
+ settings.target = {};
+ if (this.allowedConfigurations.target.link) {
+ settings.target.link = { value: this.targetLinkControl.value };
+ settings.target.openInTab = { value: this.openInTab };
+ }
+ }
+
+ buttonClass.remove('started');
+ buttonClass.add('completed');
+
+ this.dialogRef.close({success: true, settings: settings });
+ }
+
+}
diff --git a/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.html b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.html
new file mode 100644
index 00000000..8cddb189
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.html
@@ -0,0 +1,41 @@
+
+ format_color_text
+ Text color
+
+
+
+
+
+
+
+
+
+ clear
+ Cancel
+
+
+
+ format_color_reset
+ Reset color
+
+
+
+
+ save
+ Accept changes
+
+
+
+
diff --git a/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.scss b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.scss
new file mode 100644
index 00000000..fbbfce19
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.scss
@@ -0,0 +1,43 @@
+h2 mat-icon {
+ vertical-align: sub;
+ margin-right: 1ex;
+}
+
+.data .mat-form-field {
+ display: block;
+}
+
+.color-picker {
+ margin-top: 1em;
+
+ input {
+ // Mostly the same style as the one on https://huebee.buzz/
+ box-shadow: inset 0 2px 10px hsla(0, 0%, 0%, 0.15);
+ border-radius: 5px;
+ border: 1px solid hsla(0, 0%, 0%, 0.2);
+
+ padding: 0.85ex;
+ font-size: 1.2rem;
+ width: 23ex;
+
+ margin-bottom: 1ex;
+ }
+}
+
+.accept-cancel {
+ margin-top: 2em;
+}
+
+.accept-cancel button {
+ margin-right: 1ex;
+}
+
+.confirm-button {
+ background-color: #009688;
+ color: white;
+ font-weight: bold;
+}
+
+.hidden {
+ display: none;
+}
diff --git a/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.spec.ts b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.spec.ts
new file mode 100644
index 00000000..61445181
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.spec.ts
@@ -0,0 +1,57 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { CookiesService } from '@ngx-utils/cookies';
+import { BrowserCookiesModule, BrowserCookiesService } from '@ngx-utils/cookies/browser';
+import { ConfigureFontColorDialogComponent } from './configure-font-color-dialog.component';
+import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+
+
+describe('ConfigureFontColorDialogComponent', () => {
+ let component: ConfigureFontColorDialogComponent;
+ let fixture: ComponentFixture;
+
+ const mockDialogRef = {
+ close: jasmine.createSpy('close'),
+ };
+ const mockDialogData = {
+ text: 'Test!',
+ color: '#000'
+ };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ BrowserCookiesModule.forRoot(),
+ RouterTestingModule,
+ HttpClientTestingModule,
+ MatDialogModule,
+ ],
+ declarations: [ ConfigureFontColorDialogComponent ],
+ providers: [
+ {
+ provide: CookiesService,
+ useClass: BrowserCookiesService,
+ },
+ {
+ provide: MatDialogRef,
+ useValue: mockDialogRef
+ },
+ {
+ provide: MAT_DIALOG_DATA,
+ useValue: mockDialogData,
+ }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureFontColorDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.ts b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.ts
new file mode 100644
index 00000000..614d0467
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-font-color-dialog/configure-font-color-dialog.component.ts
@@ -0,0 +1,61 @@
+import { AfterViewInit, Component, ElementRef, Inject, ViewChild } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+
+declare const Huebee: any;
+
+@Component({
+ selector: 'app-configure-font-color-dialog',
+ templateUrl: './configure-font-color-dialog.component.html',
+ styleUrls: [
+ './configure-font-color-dialog.component.scss',
+ '../../../libs/css/material-icons.css',
+ ],
+})
+export class ConfigureFontColorDialogComponent implements AfterViewInit {
+ @ViewChild('textColorPicker') textColorPicker: ElementRef;
+ @ViewChild('textSampleResult') textSampleResult: ElementRef;
+ private hbTextColorPicker: any;
+
+ // Initialization
+ constructor(public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA)
+ public data: { text: string, color: string }) {
+ }
+
+ ngAfterViewInit(): void {
+ this.textSampleResult.nativeElement.innerText = this.data.text;
+
+ const startingColor = this.data.color;
+
+ // Color picker
+ this.hbTextColorPicker = new Huebee(this.textColorPicker.nativeElement, {
+ // options
+ notation: 'hex',
+ saturations: 2,
+ setText: true,
+ setBgColor: false,
+ staticOpen: true,
+ });
+ this.hbTextColorPicker.setColor(startingColor);
+
+ this.textSampleResult.nativeElement.style.color = startingColor;
+ this.hbTextColorPicker.on('change', (color: string, _hue: any, _sat: any, _lum: any) => {
+ this.textSampleResult.nativeElement.style.color = color;
+ });
+ }
+
+ // Accept/cancel
+ cancelChanges() {
+ this.dialogRef.close({success: false});
+ }
+
+ acceptChanges() {
+ this.dialogRef.close({success: true, operation: 'set-color', value: {
+ color: this.hbTextColorPicker.color,
+ }});
+ }
+
+ removeColor() {
+ this.dialogRef.close({success: true, operation: 'remove-color'});
+ }
+}
diff --git a/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.html b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.html
new file mode 100644
index 00000000..52d41283
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.html
@@ -0,0 +1,66 @@
+
+ link
+ Link
+
+
+
+
+
+
+
+ clear
+ Cancel
+
+
+
+ link_off
+ Remove link
+
+
+
+
+ save
+ Accept changes
+
+
+
+
diff --git a/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.scss b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.scss
new file mode 100644
index 00000000..d94684df
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.scss
@@ -0,0 +1,49 @@
+h2 mat-icon {
+ vertical-align: sub;
+ margin-right: 1ex;
+}
+
+.data .mat-form-field {
+ display: block;
+}
+
+.accept-cancel {
+ margin-top: 2em;
+}
+
+.accept-cancel button {
+ margin-right: 1ex;
+}
+
+.confirm-button {
+ background-color: #009688;
+ color: white;
+ font-weight: bold;
+}
+
+.hidden {
+ display: none;
+}
+
+.extra-options {
+ margin-left: 1em;
+ margin-top: 1em;
+ padding: 0;
+}
+
+.color-picker {
+ margin-top: 1em;
+
+ input {
+ // Mostly the same style as the one on https://huebee.buzz/
+ box-shadow: inset 0 2px 10px hsla(0, 0%, 0%, 0.15);
+ border-radius: 5px;
+ border: 1px solid hsla(0, 0%, 0%, 0.2);
+
+ padding: 0.85ex;
+ font-size: 1.2rem;
+ width: 23ex;
+
+ margin-bottom: 1ex;
+ }
+}
diff --git a/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.spec.ts b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.spec.ts
new file mode 100644
index 00000000..4bff532b
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.spec.ts
@@ -0,0 +1,57 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { CookiesService } from '@ngx-utils/cookies';
+import { BrowserCookiesModule, BrowserCookiesService } from '@ngx-utils/cookies/browser';
+import { ConfigureLinkDialogComponent } from './configure-link-dialog.component';
+import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+
+
+describe('ConfigureLinkDialogComponent', () => {
+ let component: ConfigureLinkDialogComponent;
+ let fixture: ComponentFixture;
+
+ const mockDialogRef = {
+ close: jasmine.createSpy('close'),
+ };
+ const mockDialogData = {
+ text: '',
+ link: ''
+ };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ BrowserCookiesModule.forRoot(),
+ RouterTestingModule,
+ HttpClientTestingModule,
+ MatDialogModule,
+ ],
+ declarations: [ ConfigureLinkDialogComponent ],
+ providers: [
+ {
+ provide: CookiesService,
+ useClass: BrowserCookiesService,
+ },
+ {
+ provide: MatDialogRef,
+ useValue: mockDialogRef
+ },
+ {
+ provide: MAT_DIALOG_DATA,
+ useValue: mockDialogData,
+ }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureLinkDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.ts b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.ts
new file mode 100644
index 00000000..c2432f12
--- /dev/null
+++ b/frontend/src/app/flow-editor/dialogs/configure-link-dialog/configure-link-dialog.component.ts
@@ -0,0 +1,117 @@
+import { Component, Inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { Validators, FormControl } from '@angular/forms';
+
+declare const Huebee: any;
+
+export const UrlPattern = new RegExp(/(https?:\/\/.{2,}\..{2,})|(mailto:.*@.*)/);
+
+export type UnderlineSettings = 'default' | 'none' | { color: string };
+
+@Component({
+ selector: 'app-configure-link-dialog',
+ templateUrl: './configure-link-dialog.component.html',
+ styleUrls: [
+ './configure-link-dialog.component.scss',
+ '../../../libs/css/material-icons.css',
+ ],
+})
+export class ConfigureLinkDialogComponent implements AfterViewInit {
+ link = new FormControl('', [Validators.required, Validators.pattern(UrlPattern)]);
+ text = new FormControl('', [Validators.required, Validators.min(1)]);
+ openInTab: boolean;
+
+ @ViewChild('underlineColorPicker') underlineColorPicker: ElementRef;
+ private hbUnderlineColorPicker: any;
+ customizeUnderline: boolean;
+ underlineColor: string;
+ noUnderline: boolean;
+
+
+ // Initialization
+ constructor(public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA)
+ public data: { link: string, text: string, openInTab: boolean, underline: UnderlineSettings }) {
+
+ this.link.setValue(data.link);
+ this.text.setValue(data.text);
+ this.openInTab = data.openInTab;
+ this.noUnderline = false;
+ this.underlineColor = '#000';
+
+ if ((data.underline) && (data.underline != 'default')) {
+ this.customizeUnderline = true;
+
+ if (data.underline === 'none') {
+ this.noUnderline = true;
+ }
+ else {
+ this.underlineColor = data.underline.color;
+ }
+ }
+ }
+
+ ngAfterViewInit(): void {
+ if (this.customizeUnderline) {
+ this.onUpdateUnderlineOptions();
+ }
+ }
+
+ getUrlErrorMessage() {
+ if (this.link.hasError('required')) {
+ return 'You must enter a value';
+ }
+
+ return this.link.hasError('pattern') ? 'Not a valid link' : '';
+ }
+
+ onUpdateUnderlineOptions() {
+ if (this.customizeUnderline && (!this.noUnderline)) {
+ setTimeout(() => {
+ // Color picker
+ this.hbUnderlineColorPicker = new Huebee(this.underlineColorPicker.nativeElement, {
+ // options
+ notation: 'hex',
+ saturations: 2,
+ setText: true,
+ setBgColor: false,
+ staticOpen: true,
+ });
+ this.hbUnderlineColorPicker.setColor(this.underlineColor);
+
+ this.hbUnderlineColorPicker.on('change', (color: string, _hue: any, _sat: any, _lum: any) => {
+ this.underlineColor = color;
+ });
+ }, 0);
+ }
+ }
+
+ // Accept/cancel
+ cancelChanges() {
+ this.dialogRef.close({success: false});
+ }
+
+ acceptChanges() {
+ let underlineSettings: UnderlineSettings = 'default';
+
+ if (this.customizeUnderline) {
+ if (this.noUnderline) {
+ underlineSettings = 'none';
+ }
+ else {
+ underlineSettings = { color: this.underlineColor };
+ }
+ }
+
+ this.dialogRef.close({success: true, operation: 'set-link', value: {
+ link: this.link.value,
+ text: this.text.value,
+ openInTab: this.openInTab,
+ underline: underlineSettings
+ }});
+ }
+
+ removeLink() {
+ this.dialogRef.close({success: true, operation: 'remove-link'});
+ }
+}
diff --git a/frontend/src/app/flow-editor/direct_value.ts b/frontend/src/app/flow-editor/direct_value.ts
new file mode 100644
index 00000000..8d06be15
--- /dev/null
+++ b/frontend/src/app/flow-editor/direct_value.ts
@@ -0,0 +1,424 @@
+import {
+ FlowBlock,
+ InputPortDefinition, OnIOSelected,
+ Area2D, Direction2D, Position2D, MessageType, FlowBlockData, FlowBlockInitOpts, FlowBlockOptions, BlockContextAction,
+} from './flow_block';
+import { BlockManager } from './block_manager';
+import { UiFlowBlock } from './ui-blocks/ui_flow_block';
+import { FlowWorkspace } from './flow_workspace';
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+export type DirectValueBlockType = 'direct_value_block';
+export const BLOCK_TYPE = 'direct_value_block';
+
+const OUTPUT_PORT_REAL_SIZE = 10;
+const MIN_WIDTH = 50;
+const OUTPUT_PORT_SIZE = 25;
+
+export type OnRequestEdit = (block: DirectValue, type: MessageType, update: (value: string) => void) => void;
+
+interface DirectValueOptions {
+ type: MessageType,
+ value: string,
+ on_io_selected?: OnIOSelected,
+ on_request_edit?: OnRequestEdit,
+};
+
+export interface DirectValueFlowBlockData extends FlowBlockData {
+ type: DirectValueBlockType,
+ value: DirectValueOptions,
+};
+
+export function isDirectValueBlockData(opt: FlowBlockData): opt is DirectValueFlowBlockData {
+ return opt.type === BLOCK_TYPE;
+}
+
+export class DirectValue implements FlowBlock {
+ options: DirectValueOptions;
+ readonly id: string;
+ readonly onMoveCallbacks: ((pos: Position2D) => void)[] = [];
+ private _workspace: FlowWorkspace;
+
+ value: string;
+ sinks: FlowBlock[] = [];
+
+ constructor(options: DirectValueOptions, blockId: string) {
+ this.options = options;
+ this.id = blockId;
+
+ this.value = options.value;
+ if (!this.value) {
+ this.value = DirectValue.getDefaultValueForType(this.options.type);
+ }
+ }
+
+ public dispose() {
+ this.canvas.removeChild(this.group);
+ }
+
+ private static getDefaultValueForType( type?: MessageType ) {
+ if (!type) { return 'sample value'; }
+
+ switch (type) {
+ case 'float':
+ return '0.5';
+ case 'integer':
+ return '9999';
+ case 'boolean':
+ return 'true';
+
+ case 'string':
+ return 'sample value';
+
+ case 'pulse':
+ case 'user-pulse':
+ console.warn('TODO: Implement pulse sender'); // Would this be implemented by a button?
+ case 'any':
+ return 'sample value';
+ }
+ }
+
+ // Render elements
+ private group: SVGGElement;
+ private node: SVGGElement;
+ private rect: SVGRectElement;
+ private rectShadow: SVGRectElement;
+ private textBox: SVGTextElement;
+ private canvas: SVGElement;
+
+ private port_external: SVGCircleElement;
+ private port_internal: SVGCircleElement;
+
+ private position: {x: number, y: number};
+ private size: { width: number, height: number };
+
+ public static GetBlockType(): string {
+ return BLOCK_TYPE;
+ }
+
+ public serialize(): DirectValueFlowBlockData {
+ const opt = JSON.parse(JSON.stringify(this.options))
+ opt.value = this.value;
+
+ return {
+ type: BLOCK_TYPE,
+ value: opt,
+ }
+ }
+
+ static Deserialize(data: FlowBlockData, blockId: string, manager: BlockManager): FlowBlock {
+ if (data.type !== BLOCK_TYPE){
+ throw new Error(`Block type mismatch, expected ${BLOCK_TYPE} found: ${data.type}`);
+ }
+
+ const options: DirectValueOptions = JSON.parse(JSON.stringify(data.value));
+ options.on_io_selected = manager.onIoSelected.bind(manager);
+ options.on_request_edit = manager.onRequestEdit.bind(manager);
+
+ return new DirectValue(options, blockId);
+ }
+
+ public getBodyElement(): SVGGraphicsElement {
+ if (!this.group) {
+ throw Error("Not rendered");
+ }
+
+ return this.node;
+ }
+
+ getBodyArea(): Area2D {
+ const rect = (this.group as any).getBBox();
+ return {
+ x: this.position.x,
+ y: this.position.y,
+ width: rect.width,
+ height: rect.height,
+ }
+ }
+
+ getValueArea(): Area2D {
+ const body = this.getBodyArea();
+
+ body.x += OUTPUT_PORT_SIZE / 2;
+ body.width -= OUTPUT_PORT_SIZE;
+
+ return body;
+ }
+
+ public getOffset(): {x: number, y: number} {
+ return {x: this.position.x, y: this.position.y};
+ }
+
+ public moveTo(pos: Position2D) {
+ this.position.x = pos.x;
+ this.position.y = pos.y;
+
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+ }
+
+ public moveBy(distance: {x: number, y: number}): FlowBlock[] {
+ if (!this.group) {
+ throw Error("Not rendered");
+ }
+
+ this.position.x += distance.x;
+ this.position.y += distance.y;
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+
+ for (const callback of this.onMoveCallbacks) {
+ callback(this.position);
+ }
+
+ return [];
+ }
+
+ public onMove(callback: (pos: Position2D) => void) {
+ this.onMoveCallbacks.push(callback);
+ }
+
+ public endMove(): FlowBlock[] {
+ return [];
+ }
+
+ public onGetFocus() {}
+ public onLoseFocus() {}
+
+ public addConnection(direction: 'in' | 'out', _index: number, block: FlowBlock): boolean {
+ if (direction === 'in') {
+ console.warn("Should NOT be possible to add a connection to a DirectValue block");
+ return false;
+ }
+
+ this.sinks.push(block);
+
+ return false;
+ }
+
+ public removeConnection(direction: 'in' | 'out', _index: number, block: FlowBlock): boolean {
+ if (direction === 'in') {
+ console.warn("Should NOT be possible to have input connections on a DirectValue block");
+ return false;
+ }
+
+ const index = this.sinks.findIndex(x => x === block);
+
+ this.sinks.splice(index, 1);
+
+ return false;
+ }
+
+ public getBlockContextActions(): BlockContextAction[] {
+ return [];
+ }
+
+ public getSlots(): {[key: string]: string} {
+ return {};
+ }
+
+ public getInputs(): InputPortDefinition[] {
+ return [];
+ }
+
+ public getPositionOfInput(index: number, edge?: boolean): Position2D {
+ throw new Error("DirectValue don't have any input");
+ }
+
+ public getPositionOfOutput(index: number, edge?: boolean): Position2D {
+ return { x: 0, y: this.size.height / 2 };
+ }
+
+ public getOutputType(_index: number): string {
+ return this.options.type;
+ }
+
+ public getInputType(_index: number): string {
+ throw Error("Direct values don't have inputs")
+ }
+
+ public getOutputRunwayDirection(): Direction2D {
+ return 'left';
+ }
+
+ public getValue() {
+ return this.value;
+ }
+
+ private setValue(new_value: string, sideChannel: boolean = false) {
+ this.value = new_value;
+
+ if (this.group) {
+ this.updateText();
+ this.updateSize();
+ }
+
+ if (this._workspace && !sideChannel) {
+ this._workspace.onBlockOptionsChanged(this);
+ }
+
+ for (const block of this.sinks) {
+ if (block instanceof UiFlowBlock) {
+ block.updateConnectionValue(this, new_value);
+ }
+ }
+ }
+
+ public updateOptions(blockData: FlowBlockData): void {
+ const data = blockData as DirectValueFlowBlockData;
+ this.setValue(data.value.value, true);
+ }
+
+ private updateText() {
+ const content = this.value || '-';
+ this.textBox.innerHTML = '';
+
+ const lines = content.split('\n')
+ for (let line of lines) {
+ if (line.length === 0) {
+ line = ' '
+ }
+ const span = document.createElementNS(SvgNS, 'tspan');
+ span.setAttributeNS(null, 'x', '0');
+ span.setAttributeNS(null, 'dy', '1.2em');
+ span.textContent = line;
+
+ this.textBox.appendChild(span);
+ }
+ }
+
+ private updateSize() {
+ const y_padding = 5; // px
+ const textArea = this.textBox.getBBox();
+
+ let widest_section = MIN_WIDTH;
+ widest_section = Math.max(widest_section, textArea.width + OUTPUT_PORT_SIZE);
+
+ const box_width = widest_section;
+ const box_height = (this.textBox.getBBox().height * 1.5 + y_padding * 2);
+
+ // Fix output port
+ const port_y_center = box_height / 2;
+
+ this.port_internal.setAttributeNS(null, 'cy', port_y_center + '');
+ this.port_external.setAttributeNS(null, 'cy', port_y_center + '');
+
+ // Center text box
+
+ this.textBox.setAttributeNS(null, 'y', (box_height - textArea.height) / 2 + "");
+ for (const line of Array.from(this.textBox.childNodes)) {
+ if (line instanceof SVGTSpanElement) {
+ line.setAttributeNS(null, 'x',
+ (OUTPUT_PORT_SIZE/4
+ + box_width/2
+ - (textArea.width/2)) + "");
+ }
+ }
+
+ // Set rect size
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+
+ this.rectShadow.setAttributeNS(null, 'width', box_width + "");
+ this.rectShadow.setAttributeNS(null, 'height', box_height + "");
+
+
+ this.size = { width: box_width, height: box_height };
+
+ }
+
+ public render(canvas: SVGElement, initOpts: FlowBlockInitOpts): SVGElement {
+ if (this.group) { return this.group }
+ this._workspace = initOpts.workspace;
+
+ this.canvas = canvas;
+ if (initOpts.position) {
+ this.position = { x: initOpts.position.x, y: initOpts.position.y };
+ }
+ else {
+ this.position = {x: 0, y: 0};
+ }
+
+ this.group = document.createElementNS(SvgNS, 'g');
+ this.node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.rectShadow = document.createElementNS(SvgNS, 'rect');
+ this.textBox = document.createElementNS(SvgNS, 'text');
+
+ this.group.setAttribute('class', 'flow_node direct_value_node');
+ this.textBox.setAttribute('class', 'node_name');
+ this.textBox.setAttributeNS(null,'textlength', '100%');
+ this.textBox.onclick = (() => {
+ if (this.options.on_request_edit) {
+ this.group.classList.add('editing');
+
+ this.options.on_request_edit(this, this.options.type || 'any',
+ (update: string) => {
+ this.setValue(update);
+
+ this.group.classList.remove('editing');
+ });
+ }
+ });
+
+ this.textBox.setAttributeNS(null, 'x', "0");
+ this.textBox.setAttributeNS(null, 'y', "0");
+
+ this.node.appendChild(this.rectShadow);
+ this.node.appendChild(this.rect);
+ this.node.appendChild(this.textBox);
+ this.group.appendChild(this.node);
+ this.canvas.appendChild(this.group);
+
+ this.updateText();
+
+ // Add direct output
+ const out_group = document.createElementNS(SvgNS, 'g');
+ this.group.appendChild(out_group);
+
+ const output_port_internal_size = 5;
+
+ let type_class = 'unknown_type';
+ if (this.options.type) {
+ type_class = this.options.type + '_port';
+ }
+
+ // Draw the output port
+ const port_x_center = 0;
+
+ this.port_external = document.createElementNS(SvgNS, 'circle');
+ this.port_external.setAttributeNS(null, 'class', 'output external_port ' + type_class);
+ this.port_external.setAttributeNS(null, 'cx', port_x_center + '');
+ this.port_external.setAttributeNS(null, 'r', OUTPUT_PORT_REAL_SIZE + '');
+
+ this.port_internal = document.createElementNS(SvgNS, 'circle');
+ this.port_internal.setAttributeNS(null, 'class', 'output internal_port');
+ this.port_internal.setAttributeNS(null, 'cx', port_x_center + '');
+ this.port_internal.setAttributeNS(null, 'r', output_port_internal_size + '');
+
+ out_group.appendChild(this.port_external);
+ out_group.appendChild(this.port_internal);
+
+ if (this.options.on_io_selected) {
+ out_group.onclick = ((_ev: MouseEvent) => {
+ this.options.on_io_selected(this, 'out', 0, { type: this.options.type }, this.getPositionOfOutput(0));
+ });
+ }
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+ this.rect.setAttributeNS(null, 'rx', "2px"); // Like border-radius, in px
+
+ this.rectShadow.setAttributeNS(null, 'class', "body_shadow");
+ this.rectShadow.setAttributeNS(null, 'x', "0");
+ this.rectShadow.setAttributeNS(null, 'y', "0");
+ this.rectShadow.setAttributeNS(null, 'rx', "2px"); // Like border-radius, in px
+
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+
+ this.updateSize();
+
+ return this.group;
+ }
+
+}
diff --git a/frontend/src/app/flow-editor/enum_direct_value.ts b/frontend/src/app/flow-editor/enum_direct_value.ts
new file mode 100644
index 00000000..c1577910
--- /dev/null
+++ b/frontend/src/app/flow-editor/enum_direct_value.ts
@@ -0,0 +1,580 @@
+import { BlockManager } from './block_manager';
+import {
+ Area2D, BlockContextAction, Direction2D, FlowBlock,
+
+ FlowBlockData, FlowBlockInitOpts, InputPortDefinition,
+ MessageType, OnIOSelected,
+ Position2D,
+ BridgeEnumInputPortDefinition,
+ BridgeEnumSequenceInputPortDefinition
+} from './flow_block';
+import { FlowWorkspace } from './flow_workspace';
+import { manageTopLevelError } from '../utils';
+import { SEPARATION } from './ui-blocks/renderers/positioning';
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+export type EnumDirectValueFlowBlockDataType = 'enum_value_block';
+export const BLOCK_TYPE = 'enum_value_block';
+
+export interface EnumDirectValueFlowBlockData {
+ type: EnumDirectValueFlowBlockDataType,
+ value: {
+ options: EnumDirectValueOptions
+ value_id: string,
+ value_text: string,
+ },
+}
+
+const OUTPUT_PORT_REAL_SIZE = 10;
+const MIN_WIDTH = 50;
+const OUTPUT_PORT_SIZE = 25;
+
+export const SEQUENCE_SEPARATOR = '\\';
+
+export type EnumValue = {
+ id: string,
+ name: string,
+};
+
+export type EnumGetter = (namespace: string, name: string, selector?: string) => EnumValue[] | Promise;
+
+export type OnSelectRequested = ((block: FlowBlock,
+ previous_value: string,
+ values: EnumValue[],
+ value_dict: {[key:string]: EnumValue},
+ update: (new_value: string) => void,
+ ) => void);
+
+export interface EnumDirectValueOptions {
+ definition: BridgeEnumInputPortDefinition | BridgeEnumSequenceInputPortDefinition,
+ get_values: EnumGetter;
+ type?: MessageType,
+ on_io_selected?: OnIOSelected,
+ on_select_requested?: OnSelectRequested,
+};
+
+export function isEnumDirectValueBlockData(opt: FlowBlockData): opt is EnumDirectValueFlowBlockData {
+ return opt.type === BLOCK_TYPE;
+}
+
+function tagDepth(parent: string, list: EnumValue[]): EnumValue[] {
+ const newValues = [] as EnumValue[];
+ for (const item of list) {
+ newValues.push({
+ name: item.name,
+ id: `${parent}${SEQUENCE_SEPARATOR}${item.id}`,
+ });
+ }
+
+ return newValues;
+}
+
+
+export class EnumDirectValue implements FlowBlock {
+ options: EnumDirectValueOptions;
+ readonly id: string;
+ readonly onMoveCallbacks: ((pos: Position2D) => void)[] = [];
+ private _workspace: FlowWorkspace;
+
+ value_id: string = undefined;
+
+ values: EnumValue[];
+ value_dict: {[key:string]: EnumValue};
+
+ constructor(options: EnumDirectValueOptions, blockId: string) {
+ this.options = options;
+ this.id = blockId;
+ }
+
+ public dispose() {
+ this.canvas.removeChild(this.group);
+ }
+
+ // Render elements
+ private group: SVGGElement;
+ private node: SVGGElement;
+ private rect: SVGRectElement;
+ private rectShadow: SVGRectElement;
+ private textBox: SVGTextElement;
+ private canvas: SVGElement;
+ private _defaultText: string;
+
+ private position: {x: number, y: number};
+ private textCorrection: {x: number, y: number};
+ private size: { width: number, height: number };
+
+ public static GetBlockType(): string {
+ return BLOCK_TYPE;
+ }
+
+ public serialize(): EnumDirectValueFlowBlockData {
+ return {
+ type: BLOCK_TYPE,
+ value: { options: JSON.parse(JSON.stringify(this.options)),
+ value_id: this.getValue(),
+ value_text: this.textBox.textContent,
+ },
+ }
+ }
+
+ static Deserialize(data: FlowBlockData, blockId: string, manager: BlockManager, enumGetter: EnumGetter): FlowBlock {
+ if (data.type !== BLOCK_TYPE){
+ throw new Error(`Block type mismatch, expected ${BLOCK_TYPE} found: ${data.type}`);
+ }
+
+ const options: EnumDirectValueOptions = JSON.parse(JSON.stringify(data.value.options));
+
+ // Port over from past structure
+ if (!options.definition) {
+ options.definition = {
+ type: 'enum',
+ enum_name: (options as any).enum_name,
+ enum_namespace: (options as any).enum_namespace,
+ }
+ delete (options as any).enum_name;
+ delete (options as any).enum_namespace;
+ }
+
+ options.on_io_selected = manager.onIoSelected.bind(manager);
+ options.on_select_requested = manager.onSelectRequested.bind(manager);
+ options.get_values = enumGetter;
+
+ const block = new EnumDirectValue(options, blockId);
+
+ block.value_id = data.value.value_id;
+ block._defaultText = data.value.value_text;
+
+ return block;
+ }
+
+ public getBodyElement(): SVGGraphicsElement {
+ if (!this.group) {
+ throw Error("Not rendered");
+ }
+
+ return this.node;
+ }
+
+ getBodyArea(): Area2D {
+ const rect = (this.group as any).getBBox();
+ return {
+ x: this.position.x,
+ y: this.position.y,
+ width: rect.width,
+ height: rect.height,
+ }
+ }
+
+ getValueArea(): Area2D {
+ const body = this.getBodyArea();
+
+ body.x += OUTPUT_PORT_SIZE / 2;
+ body.width -= OUTPUT_PORT_SIZE;
+
+ return body;
+ }
+
+ public getOffset(): {x: number, y: number} {
+ return {x: this.position.x, y: this.position.y};
+ }
+
+ public moveTo(pos: Position2D) {
+ this.position.x = pos.x;
+ this.position.y = pos.y;
+
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+ }
+
+ public moveBy(distance: {x: number, y: number}): FlowBlock[] {
+ if (!this.group) {
+ throw Error("Not rendered");
+ }
+
+ this.position.x += distance.x;
+ this.position.y += distance.y;
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+
+ for (const callback of this.onMoveCallbacks) {
+ callback(this.position);
+ }
+
+ return [];
+ }
+
+ public onMove(callback: (pos: Position2D) => void) {
+ this.onMoveCallbacks.push(callback);
+ }
+
+ public endMove(): FlowBlock[] {
+ return [];
+ }
+
+ public onGetFocus() {}
+ public onLoseFocus() {}
+
+ public addConnection(direction: 'in' | 'out', _index: number): boolean {
+ if (direction === 'in') {
+ console.warn("Should NOT be possible to add a connection to a EnumDirectValue block");
+ }
+
+ return false;
+ }
+
+ public removeConnection(_direction: 'in' | 'out', _index: number) : boolean {
+ return false;
+ }
+
+ public getBlockContextActions(): BlockContextAction[] {
+ return [];
+ }
+
+ public getSlots(): {[key: string]: string} {
+ return {};
+ }
+
+ public getInputs(): InputPortDefinition[] {
+ return [];
+ }
+
+ public getPositionOfInput(index: number, edge?: boolean): Position2D {
+ throw new Error("EnumDirectValue don't have any input");
+ }
+
+ public getPositionOfOutput(index: number, edge?: boolean): Position2D {
+ return { x: 0, y: this.size.height / 2 };
+ }
+
+ public getOutputType(_index: number): string {
+ return this.options.type || 'enum';
+ }
+
+ public getInputType(_index: number): string {
+ throw Error("Direct enum values don't have inputs")
+ }
+
+ public getOutputRunwayDirection(): Direction2D {
+ return 'left';
+ }
+
+ public getValue() {
+ return this.value_id;
+ }
+
+ private setValue(id: string, sideChannel: boolean =false) {
+ this.value_id = id;
+
+ if (!this.value_dict) { return; }
+
+ const selected = this.value_dict[id];
+
+ if (this.group) {
+ this.textBox.textContent = (selected && selected.name) || '-';
+ this.updateSize();
+ }
+
+ if (this._workspace && !sideChannel) {
+ this._workspace.onBlockOptionsChanged(this);
+ }
+ }
+
+ public updateOptions(blockData: FlowBlockData): void {
+ const data = blockData as EnumDirectValueFlowBlockData;
+ this.setValue(data.value.value_id, true);
+ this.textBox.textContent = data.value.value_text;
+ }
+
+ static cleanSequenceValue(value: string): string {
+ const chunks = value.split(SEQUENCE_SEPARATOR);
+ return chunks[chunks.length - 1];
+ }
+
+ private loadValues() {
+
+ const selectValue = (id: string) => {
+ this.group.classList.remove('editing');
+ const oldValue = this.value_id;
+ this.setValue(id);
+
+ if (this.options.definition.type === 'enum') {
+ return;
+ }
+ else if (this.options.definition.type !== 'enum_sequence') {
+ throw Error(`Unknown enum type: ${(this.options.definition as any).type}`);
+ }
+
+ if (id === 'Select') {
+ on_done([{ id: "Select", name: 'Not found' }])
+ return;
+ }
+
+ if ((id === oldValue) && (this.values)) {
+ console.debug("Skipping reload on", oldValue, '->', id);
+ return;
+ }
+
+
+ const chunks = id ? id.split(SEQUENCE_SEPARATOR) : [];
+
+ const foundName = this.values ? this.values.find((value: EnumValue) => value.id === id) : null;
+ let selectedName = foundName ? foundName.name : null;
+ if (selectedName && selectedName.match(/Go back [0-9]+ steps?/)) {
+ // TODO: Properly extract this name
+ selectedName = null;
+ }
+
+ const selectedDepth = chunks.length;
+
+ if (selectedDepth === 0) {
+ this.setValue('Select');
+ const result = this.options.get_values(this.options.definition.enum_namespace, this.options.definition.enum_sequence[0]);
+
+ if ((result as any).then) {
+ (result as Promise).then(on_done);
+ }
+ else {
+ on_done(result as EnumValue[]);
+ }
+
+ return;
+ }
+ else {
+ const depth = selectedDepth < this.options.definition.enum_sequence.length
+ ? selectedDepth
+ : this.options.definition.enum_sequence.length - 1;
+
+ let _fullRef: string[];
+ if (depth === selectedDepth) {
+ _fullRef = chunks;
+ }
+ else {
+ _fullRef = chunks.slice(0, chunks.length - 1); // Parent
+ }
+ const fullReference = _fullRef.join(SEQUENCE_SEPARATOR);
+ const lastLevel = _fullRef[_fullRef.length - 1];
+
+ const result = this.options.get_values(this.options.definition.enum_namespace,
+ this.options.definition.enum_sequence[depth],
+ lastLevel);
+
+ const loopNext = manageTopLevelError((values: EnumValue[]) => {
+
+ const prelude : EnumValue[] = [ { name: "Back to Top", id: '' } ];
+ for (let i = 1; i < depth; i++ ) {
+ prelude.push({
+ name: `Go back ${i} step` + (i === 1 ? '' : 's'),
+ id: id.split(SEQUENCE_SEPARATOR, depth - i).join(SEQUENCE_SEPARATOR),
+ });
+ }
+
+ const newName = selectedName ? `Select in ${selectedName}` : 'Select';
+ if (depth === selectedDepth) {
+ // This is not needed if we're "seeing" it from another level
+ prelude.push({ name: newName, id: id });
+ }
+
+ const menu = prelude.concat(tagDepth(fullReference, values));
+
+ on_done(menu);
+ this.setValue(id);
+ });
+
+ if ((result as any).then) {
+ (result as Promise).then(loopNext);
+ }
+ else {
+ loopNext(result as EnumValue[]);
+ }
+ }
+ };
+
+ const initialize = () => {
+ const startEditing = manageTopLevelError(() => {
+ if (this.options.on_select_requested) {
+ this.group.classList.add('editing');
+
+ this.options.on_select_requested(
+ this, this.value_id, this.values, this.value_dict,
+ manageTopLevelError(selectValue)
+ );
+ }
+ });
+
+ this.textBox.onclick = startEditing;
+ this.getBodyElement().ontouchend = startEditing;
+ };
+
+ const on_done = (values: EnumValue[]) => {
+ this.values = values;
+
+ this.value_dict = {};
+ for (const value of values) {
+ this.value_dict[value.id] = value;
+ }
+
+ initialize();
+
+ if (this.value_id === undefined) {
+ this.setValue(values[0].id);
+ }
+ }
+
+ if (this.value_id !== undefined) {
+ this.textBox.textContent = this._defaultText;
+ }
+
+ if (this.options.definition.type === 'enum') {
+ const result = this.options.get_values(this.options.definition.enum_namespace, this.options.definition.enum_name);
+ if ((result as any).then) {
+ (result as Promise).then(on_done);
+ }
+ else {
+ on_done(result as EnumValue[]);
+ }
+ }
+ else if (this.options.definition.type === 'enum_sequence') {
+ selectValue(this.value_id || '');
+ }
+ else {
+ throw Error(`Unknown enum type: ${(this.options.definition as any).type}`);
+ }
+ }
+
+ private updateSize() {
+ let widest_section = MIN_WIDTH;
+ widest_section = Math.max(widest_section, (this.textBox as any).getBBox().width + OUTPUT_PORT_SIZE);
+
+ const box_width = widest_section;
+
+ // Center text box
+ this.textBox.setAttributeNS(null, 'x', (this.textCorrection.x
+ + OUTPUT_PORT_SIZE/4
+ + box_width/2
+ - ((this.textBox as any).getBBox().width/2)) + "");
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+ this.rectShadow.setAttributeNS(null, 'width', box_width + "");
+ }
+
+ public render(canvas: SVGElement, initOpts: FlowBlockInitOpts): SVGElement {
+ if (this.group) { return this.group }
+ this._workspace = initOpts.workspace;
+
+ this.canvas = canvas;
+ if (initOpts.position) {
+ this.position = { x: initOpts.position.x, y: initOpts.position.y };
+ }
+ else {
+ this.position = {x: 0, y: 0};
+ }
+
+ const y_padding = 5; // px
+
+ this.group = document.createElementNS(SvgNS, 'g');
+ this.node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.rectShadow = document.createElementNS(SvgNS, 'rect');
+ this.textBox = document.createElementNS(SvgNS, 'text');
+
+ this.group.setAttribute('class', 'flow_node direct_value_node');
+ this.textBox.setAttribute('class', 'node_name');
+ this.textBox.textContent = "Loading...";
+ this.textBox.setAttributeNS(null,'textlength', '100%');
+
+ this.textBox.setAttributeNS(null, 'x', "0");
+ this.textBox.setAttributeNS(null, 'y', "0");
+
+ this.node.appendChild(this.rectShadow);
+ this.node.appendChild(this.rect);
+ this.node.appendChild(this.textBox);
+ this.group.appendChild(this.node);
+ this.canvas.appendChild(this.group);
+
+ // Read text correction
+ this.textCorrection = {
+ x: -(this.textBox.getBoundingClientRect().left - this.node.getBoundingClientRect().left),
+ y: -(this.textBox.getBoundingClientRect().top - this.node.getBoundingClientRect().top)
+ };
+
+ const box_height = (this.textBox.getBoundingClientRect().height * 2 + y_padding * 2);
+
+ // Add direct output
+ const out_group = document.createElementNS(SvgNS, 'g');
+ this.group.appendChild(out_group);
+
+ const output_port_internal_size = 5;
+
+ let type_class = 'unknown_type';
+ if (this.options.type) {
+ type_class = this.options.type + '_port';
+ }
+
+ // Draw the output port
+ const port_x_center = 0;
+ const port_y_center = box_height / 2;
+
+ const port_external = document.createElementNS(SvgNS, 'circle');
+ port_external.setAttributeNS(null, 'class', 'output external_port ' + type_class);
+ port_external.setAttributeNS(null, 'cx', port_x_center + '');
+ port_external.setAttributeNS(null, 'cy', port_y_center + '');
+ port_external.setAttributeNS(null, 'r', OUTPUT_PORT_REAL_SIZE + '');
+
+ const port_internal = document.createElementNS(SvgNS, 'circle');
+ port_internal.setAttributeNS(null, 'class', 'output internal_port');
+ port_internal.setAttributeNS(null, 'cx', port_x_center + '');
+ port_internal.setAttributeNS(null, 'cy', port_y_center + '');
+ port_internal.setAttributeNS(null, 'r', output_port_internal_size + '');
+
+ out_group.appendChild(port_external);
+ out_group.appendChild(port_internal);
+
+ if (this.options.on_io_selected) {
+ out_group.onclick = ((_ev: MouseEvent) => {
+ this.options.on_io_selected(this, 'out', 0, { type: this.options.type },
+ { x: port_x_center, y: port_y_center });
+ });
+ }
+
+ let widest_section = MIN_WIDTH;
+ widest_section = Math.max(widest_section, this.textBox.getBoundingClientRect().width + OUTPUT_PORT_SIZE);
+
+ const box_width = widest_section;
+
+ // Center text box
+ this.textBox.setAttributeNS(null, 'x', (this.textCorrection.x
+ + OUTPUT_PORT_SIZE/4
+ + box_width/2
+ - ((this.textBox as any).getBBox().width/2)) + "");
+ this.textBox.setAttributeNS(null, 'y', ((this.textBox as any).getBBox().height*1.75 + this.textCorrection.y) + "");
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+ this.rect.setAttributeNS(null, 'rx', "2px"); // Like border-radius, in px
+
+
+ this.rectShadow.setAttributeNS(null, 'class', "body_shadow");
+ this.rectShadow.setAttributeNS(null, 'x', "0");
+ this.rectShadow.setAttributeNS(null, 'y', "0");
+ this.rectShadow.setAttributeNS(null, 'width', box_width + "");
+ this.rectShadow.setAttributeNS(null, 'height', box_height + "");
+ this.rectShadow.setAttributeNS(null, 'rx', "2px"); // Like border-radius, in px
+
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+
+ this.size = { width: box_width, height: box_height };
+
+ try {
+ this.loadValues();
+ }
+ catch (err) {
+ console.error("Error loading enum values:", err);
+ }
+
+ this.updateSize();
+
+ return this.group;
+ }
+
+}
diff --git a/frontend/src/app/flow-editor/flow-editor.component.html b/frontend/src/app/flow-editor/flow-editor.component.html
new file mode 100644
index 00000000..7393d509
--- /dev/null
+++ b/frontend/src/app/flow-editor/flow-editor.component.html
@@ -0,0 +1,176 @@
+
diff --git a/frontend/src/app/flow-editor/flow-editor.component.scss b/frontend/src/app/flow-editor/flow-editor.component.scss
new file mode 100644
index 00000000..00eb9eeb
--- /dev/null
+++ b/frontend/src/app/flow-editor/flow-editor.component.scss
@@ -0,0 +1,135 @@
+.program-pad #workspace {
+ width: 100%;
+}
+
+.app-content {
+ overflow: hidden;
+}
+
+#program-header {
+ border-bottom: 1px solid #AAA;
+ overflow-y: auto;
+ height: 3em;
+ box-sizing: content-block;
+}
+
+#program-header:not(.is-scrollable) > .hint-scrollable {
+ display: none;
+}
+
+#program-header.is-scrollable > .hint-scrollable {
+ position: absolute;
+ top: 1ex;
+ right: 1ex;
+ z-index: 10;
+}
+
+#program-header > .hint-scrollable > mat-icon {
+ background: rgba(255,255,255,0.8);
+ border-radius: 22px;
+}
+
+#program-header > .hint-scrollable > .hint-text {
+ display: none;
+
+ position: absolute;
+ z-index: 10;
+ margin-left: -25ex;
+ padding: 0.5ex;
+ background: rgba(0,0,0,0.8);
+ color: white;
+ border-radius: 5px;
+ top: -0.5ex;
+ width: 25ex;
+ text-align: center;
+ pointer-events: none;
+}
+
+#program-header > .hint:hover > .hint-text {
+ display: block;
+}
+
+#sidepanel {
+ max-width: 30em;
+ display: block;
+}
+
+button#program-visibility-state,
+button#program-clone-button,
+button#program-rename-button,
+button#program-start-button,
+button#program-delete-button,
+button#program-logs-button,
+button#advancedProgramControls,
+button#navigateToPageControls,
+button#addResponsiveGridControls {
+
+ vertical-align: top;
+ padding: 1ex;
+ margin-left: 0.5ex;
+
+ &.annotated-icon {
+ padding: 0.25ex;
+ margin-bottom: 1px;
+ }
+}
+
+button.dangerous {
+ background-color: #FAA;
+
+ mat-icon {
+ color: #000;
+ }
+
+ &:hover {
+ color: #fff;
+ background-color: #F44336;
+
+ mat-icon {
+ color: #fff;
+ }
+ }
+}
+
+button#program-delete-button{
+ float: right;
+ margin-right: 1px;
+}
+
+button#program-start-button .action-icon {
+ vertical-align: top;
+}
+
+.program-name .program-title {
+ vertical-align: middle;
+}
+
+.program-title {
+ font-size: 1.15rem;
+}
+
+.program-name {
+ display: inline;
+ padding-right: 0.25rem;
+}
+
+.program-name > a {
+ padding-left: 1ex;
+}
+
+.program-name > .program-title {
+ display: inline-block;
+ width: 15ex;
+ max-width: 40vw;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.program-name .back-arrow {
+ vertical-align: middle;
+}
+
+.viewer {
+ margin: 0;
+}
diff --git a/frontend/src/app/flow-editor/flow-editor.component.ts b/frontend/src/app/flow-editor/flow-editor.component.ts
new file mode 100644
index 00000000..6cc73d30
--- /dev/null
+++ b/frontend/src/app/flow-editor/flow-editor.component.ts
@@ -0,0 +1,716 @@
+import { Location, isPlatformServer } from '@angular/common';
+import {switchMap} from 'rxjs/operators';
+import { Component, Input, OnInit, ViewChild, Inject, PLATFORM_ID, AfterViewInit } from '@angular/core';
+import { ActivatedRoute, Params, Router } from '@angular/router';
+import { ProgramContent, ProgramLogEntry, ProgramInfoUpdate, ProgramType, VisibilityEnum } from '../program';
+import { ProgramService } from '../program.service';
+
+import * as progbar from '../ui/progbar';
+import { Toolbox } from './toolbox'
+import { fromCustomBlockService } from './toolbox_builder';
+
+import { FlowWorkspace } from './flow_workspace';
+
+import { CustomBlockService } from '../custom_block.service';
+
+import { MatDialog } from '@angular/material/dialog';
+import { MatDrawer } from '@angular/material/sidenav';
+
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { RenameProgramDialogComponent } from '../RenameProgramDialogComponent';
+import { DeleteProgramDialogComponent } from '../DeleteProgramDialogComponent';
+import { StopThreadProgramDialogComponent } from '../StopThreadProgramDialogComponent';
+import { SetProgramTagsDialogComponent } from '../program_tags/SetProgramTagsDialogComponent';
+import { ServiceService } from '../service.service';
+import { ConnectionService } from '../connection.service';
+import { SessionService } from '../session.service';
+import { unixMsToStr } from '../utils';
+import { Session } from '../session';
+import { FlowGraph } from './flow_graph';
+import { EnumValue } from './enum_direct_value';
+import { compile } from './graph_analysis';
+import { BrowserService } from 'app/browser.service';
+import { EnvironmentService } from 'app/environment.service';
+import { UiSignalService } from 'app/services/ui-signal.service';
+import { ContainerFlowBlock } from './ui-blocks/container_flow_block';
+import { UI_ICON } from './definitions';
+import { ResponsivePageBuilder, ResponsivePageGenerateTree } from './ui-blocks/renderers/responsive_page';
+import { ChangeProgramVisilibityDialog } from '../dialogs/change-program-visibility-dialog/change-program-visibility-dialog.component';
+import { CloneProgramDialogComponentData, CloneProgramDialogComponent } from '../dialogs/clone-program-dialog/clone-program-dialog.component';
+import { uuidv4 } from './utils';
+import { EnvironmentDefinition } from 'environments/environment-definition';
+import { environment } from 'environments/environment';
+import { ToastrService } from 'ngx-toastr';
+import { Subscription } from 'rxjs';
+import { ProgramEditorSidepanelComponent } from 'app/components/program-editor-sidepanel/program-editor-sidepanel.component';
+import { HttpClient } from '@angular/common/http';
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+@Component({
+ selector: 'app-my-flow-editor',
+ templateUrl: './flow-editor.component.html',
+ styleUrls: [
+ 'flow-editor.component.scss',
+ '../libs/css/material-icons.css',
+ '../libs/css/bootstrap.min.css',
+ ],
+})
+export class FlowEditorComponent implements OnInit, AfterViewInit {
+ @Input() program: ProgramContent;
+ @ViewChild('drawer') drawer: MatDrawer;
+ @ViewChild('sidepanel') sidepanel: ProgramEditorSidepanelComponent;
+
+ session: Session;
+ programId: string;
+ environment: EnvironmentDefinition;
+ workspace: FlowWorkspace;
+ toolbox: Toolbox;
+
+ portraitMode: boolean;
+ smallScreen: boolean;
+ pages: { name: string; url: string; }[];
+ workspaceElement: HTMLElement;
+ read_only: boolean = true;
+ can_admin: boolean = false;
+ visibility: VisibilityEnum;
+ mutationObserver: MutationObserver | null;
+
+ constructor(
+ private browser: BrowserService,
+
+ private programService: ProgramService,
+ private customBlockService: CustomBlockService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private location: Location,
+ private dialog: MatDialog,
+ private serviceService: ServiceService,
+ private notification: MatSnackBar,
+ private connectionService: ConnectionService,
+ private sessionService: SessionService,
+ private uiSignalService: UiSignalService,
+ private environmentService: EnvironmentService,
+ private toastr: ToastrService,
+ private http: HttpClient,
+
+ @Inject(PLATFORM_ID) private platformId: Object
+ ) {
+ }
+
+ ngOnInit(): Promise {
+ this.environment = environment;
+
+ if (isPlatformServer(this.platformId)) {
+ // This cannot be rendered on server, so halt it's load
+ return;
+ }
+
+ if (this.browser.window && (this.browser.window.innerWidth < this.browser.window.innerHeight)) {
+ this.portraitMode = true;
+ } else {
+ this.portraitMode = false;
+ }
+ this.smallScreen = this.browser.window.innerWidth < 750;
+
+ return progbar.track(new Promise((resolve, reject) => {
+ this.sessionService.getSession()
+ .then((session) => {
+ this.session = session;
+
+ this.route.params.pipe(
+ switchMap((params: Params) => {
+ this.programId = params['program_id'];
+
+ // Note that configuring the UiSignal this way means
+ // that it can be in a semi-initialized state, which
+ // is not good. This should be fixed in the future
+ // if we still need this same data.
+ this.uiSignalService.setProgramId(this.programId);
+
+ return this.programService.getProgramById(params['program_id']).catch(err => {
+ if (!session.active) {
+ // Trying to read a program without a session, login
+ this.router.navigate(['/login'], {replaceUrl: true});
+ reject();
+ this.toastr.error(err.message, "Error loading");
+ throw Error("Error loading");
+ }
+ else {
+ // Just go back
+ // TODO: Show an appropriate error
+
+ console.error("Error:", err);
+ this.toastr.error(err.message, "Error loading");
+ reject();
+ throw Error("Error loading");
+ }
+ });
+ }))
+ .subscribe(program => {
+ this.program = program;
+ this.read_only = program.readonly;
+ this.visibility = program.visibility;
+ this.can_admin = program.can_admin;
+
+ this.prepareWorkspace().then(() => {
+ this.load_program(program);
+ resolve();
+ }).catch(err => {
+ console.error(err);
+ resolve();
+ this.toastr.error(err, "Error loading");
+ });
+ });
+ })
+ .catch(err => {
+ console.error("Error loading program:", err);
+ reject();
+ this.toastr.error(err, "Error loading");
+ });
+ }));
+ }
+
+ ngAfterViewInit() {
+ const elem = (this.drawer as any)._elementRef.nativeElement;
+
+ this.mutationObserver = new MutationObserver(() => {
+ this.notifyResize();
+
+ // HACK: Wait for animations to finish
+ for (let delay = 200; delay < 1000; delay *= 2 ) {
+ setTimeout(() => {
+ this.notifyResize();
+ }, delay);
+ }
+ });
+ this.mutationObserver.observe(elem, { attributes: true, subtree: true });
+ }
+
+ load_program(program: ProgramContent) {
+ if (program.orig && program.orig !== 'undefined') {
+ this.workspace.load(program.orig as FlowGraph);
+
+ console.time("Positioning");
+ this.workspace.repositionIteratively().then(() => console.timeEnd("Positioning"));
+ }
+ else {
+ this.workspace.initializeEmpty();
+ }
+
+ // For debugging
+ (window as any).reposition = this.workspace.repositionAll.bind(this.workspace);
+ (window as any).repositionIt = this.workspace.repositionIteratively.bind(this.workspace);
+
+ this.workspace.center();
+
+ const pages = this.workspace.getPages();
+ this.updateViewPages(Object.keys(pages));
+ }
+
+ addResponsivePage() {
+ const block = new ContainerFlowBlock({
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ subtype: 'container_flow_block',
+ id: 'responsive_page_holder',
+ builder: ResponsivePageBuilder,
+ gen_tree: ResponsivePageGenerateTree,
+ isPage: true,
+ }, uuidv4(), this.uiSignalService);
+
+ const blockId = this.workspace.draw(block);
+
+ this.workspace.centerOnBlock(blockId);
+ }
+
+ updateViewPages(pages: string[]) {
+ this.pages = pages.map(page => { return { name: page, url: this.programService.getPageUrl(this.programId, page) } });
+ }
+
+ openDefaultPage() {
+ const url = this.programService.getPageUrl(this.programId, '/');
+ let res = window.open(url,'_blank', 'noopener,noreferrer');
+ }
+
+ async prepareWorkspace(): Promise {
+ // For consistency and because it affects the positioning of the bottom drawer.
+ this.reset_header_scroll();
+
+ await this.injectWorkspace();
+ }
+
+ async injectWorkspace() {
+ this.workspaceElement = document.getElementById('workspace');
+ const programHeaderElement = document.getElementById('program-header');
+
+ this.browser.window.onresize = (() => {
+ this.calculate_size(this.workspaceElement);
+ this.calculate_program_header_size(programHeaderElement);
+ this.workspace.onResize();
+ this.toolbox.onResize();
+ });
+ this.calculate_size(this.workspaceElement);
+ this.calculate_program_header_size(programHeaderElement);
+
+ this.workspace = FlowWorkspace.BuildOn(this.workspaceElement,
+ this.getEnumValues.bind(this),
+ this.dialog,
+ this.programId,
+ this.programService,
+ this.read_only,
+ this.sessionService,
+ this.environmentService,
+ this.toastr,
+ );
+ this.toolbox = await fromCustomBlockService(this.workspaceElement, this.workspace,
+ this.customBlockService,
+ this.serviceService,
+ this.environmentService,
+ this.program.id,
+ this.uiSignalService,
+ this.connectionService,
+ this.session,
+ this.dialog,
+ this.reloadToolbox.bind(this),
+ this.read_only,
+ { portrait: this.portraitMode, autohide: this.smallScreen },
+ );
+ this.workspace.setToolbox(this.toolbox);
+ }
+
+ async reloadToolbox() {
+ const old = this.toolbox;
+ this.toolbox = null;
+ old.dispose();
+
+ this.toolbox = await fromCustomBlockService(this.workspaceElement, this.workspace,
+ this.customBlockService,
+ this.serviceService,
+ this.environmentService,
+ this.program.id,
+ this.uiSignalService,
+ this.connectionService,
+ this.session,
+ this.dialog,
+ this.reloadToolbox.bind(this),
+ this.read_only,
+ { portrait: this.portraitMode, autohide: this.smallScreen },
+ );
+ this.workspace.setToolbox(this.toolbox);
+ }
+
+ async getEnumValues(enum_namespace: string, enum_name: string, selector?: string): Promise {
+ if (enum_namespace === 'programaker') {
+ if (enum_name === 'bridges') {
+ const connections = await this.connectionService.getConnectionsOnProgram(this.programId);
+
+ const knownBridges: {[key: string]: boolean} = {};
+ const dropdown = [];
+ for (const conn of connections) {
+ if (!knownBridges[conn.bridge_id]) {
+ knownBridges[conn.bridge_id] = true;
+ dropdown.push({ id: conn.bridge_id, name: conn.bridge_name } );
+ }
+ }
+ return dropdown;
+ }
+ }
+ else {
+ let values;
+ if (!selector) {
+ values = await this.customBlockService.getCallbackOptions(this.program.id, enum_namespace, enum_name);
+ }
+ else {
+ values = await this.customBlockService.getCallbackOptionsOnSequence(this.program.id, enum_namespace, enum_name, selector);
+ }
+
+ return values.map(v => {
+ return {
+ id: v[1], name: v[0],
+ }
+ });
+ }
+ }
+
+ calculate_size(workspace: HTMLElement) {
+ const header = document.getElementById('program-header');
+ if (!header) { return; }
+ const header_pos = this.get_position(header);
+ const header_end = header_pos.y + header.clientHeight;
+
+ const window_height = Math.max(document.documentElement.clientHeight, this.browser.window.innerHeight || 0);
+
+ workspace.style.height = (window_height - header_end - 1) + 'px';
+ }
+
+ calculate_program_header_size(programHeader: HTMLElement) {
+ const isScrollable = programHeader.clientHeight < programHeader.scrollHeight;
+ if (!isScrollable) {
+ programHeader.classList.remove('is-scrollable');
+ }
+ else {
+ programHeader.classList.add('is-scrollable');
+ }
+ }
+
+ get_position(element: any): { x: number, y: number } {
+ let xPosition = 0;
+ let yPosition = 0;
+
+ while (element) {
+ xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
+ yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
+ element = element.offsetParent;
+ }
+
+ return { x: xPosition, y: yPosition };
+ }
+
+ reset_header_scroll() {
+ document.getElementById('program-header').scrollTo(0, 0);
+ }
+
+ goBack(): boolean {
+ this.dispose();
+ this.location.back();
+ return false;
+ }
+
+ dispose() {
+ if (this.workspace) {
+ this.workspace.dispose();
+ }
+
+ if (this.sidepanel) {
+ this.sidepanel.dispose();
+ }
+
+ this.workspace = null;
+ }
+
+ async sendProgram(): Promise {
+ const graph = this.workspace.getGraph();
+ const pages = this.workspace.getPages();
+
+ const t0 = new Date();
+ let compiled_program;
+ try {
+ compiled_program = compile(graph);
+ }
+ catch (error) {
+ this.toastr.error(error, 'Invalid program', {
+ closeButton: true,
+ progressBar: true,
+ });
+
+ console.error(error);
+ return;
+ }
+ this.updateViewPages(Object.keys(pages));
+
+ console.debug('Compilation time:', (new Date() as any) - (t0 as any), 'ms')
+
+ // Send update
+ const button = document.getElementById('program-start-button');
+ if (button){
+ button.classList.add('started');
+ button.classList.remove('completed');
+ }
+
+ const program = {
+ type: 'flow_program' as ProgramType,
+ parsed: { blocks: compiled_program, variables: [] as [] },
+ pages: pages,
+ orig: graph,
+ id: this.programId,
+ };
+
+ const result = await this.programService.updateProgramById(program);
+
+ if (button){
+ button.classList.remove('started');
+ button.classList.add('completed');
+ }
+
+ if (result) {
+ this.toastr.success('Upload complete', '', {
+ closeButton: true,
+ progressBar: true,
+ });
+ }
+ else {
+ this.toastr.error('Error on upload', '', {
+ closeButton: true,
+ progressBar: true,
+ });
+ }
+
+ return result;
+ }
+
+ cloneProgram() {
+ const programData: CloneProgramDialogComponentData = {
+ name: this.program.name,
+ program: JSON.parse(JSON.stringify(this.program)),
+ };
+
+ programData.program.orig = this.workspace.getGraph();
+ if (((!programData.program.parsed) || (programData.program.parsed === 'undefined'))) {
+ programData.program.parsed = { blocks: [], variables: [] };
+ }
+
+ const dialogRef = this.dialog.open(CloneProgramDialogComponent, {
+ data: programData
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ const program_id = result.program_id;
+ this.dispose();
+ this.router.navigate([`/programs/${program_id}/flow`], { replaceUrl: false });
+ });
+ }
+
+ renameProgram() {
+ const programData = { name: this.program.name };
+
+ const dialogRef = this.dialog.open(RenameProgramDialogComponent, {
+ data: programData
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ await this.sendProgram();
+ const rename = (this.programService.renameProgramById(this.program.id, programData.name)
+ .catch(() => { return false; })
+ .then(success => {
+ if (!success) {
+ return;
+ }
+
+ this.notification.open('Renamed successfully', 'ok', {
+ duration: 5000
+ });
+ }));
+ progbar.track(rename);
+ });
+ }
+
+ changeVisibility() {
+ const data = {
+ name: this.program.name,
+ visibility: this.visibility
+ };
+
+ const dialogRef = this.dialog.open(ChangeProgramVisilibityDialog, {
+ data: data
+ });
+
+
+ dialogRef.afterClosed().subscribe((result: { visibility: VisibilityEnum } | null) => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ const vis = result.visibility;
+ this.programService.updateProgramVisibility( this.program.id, { visibility: vis } ).then(() => {
+ this.visibility = vis;
+ });
+
+ });
+ }
+
+ setProgramTags() {
+ const data = {
+ program: this.program,
+ user_id: this.program.owner,
+ tags: [] as string[], // Initially empty, to be updated by dialog
+ };
+
+ const dialogRef = this.dialog.open(SetProgramTagsDialogComponent, {
+ data: data
+ });
+
+ dialogRef.afterClosed().subscribe(result => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ const update = (this.programService.updateProgramTags(this.program.id, data.tags)
+ .then((success) => {
+ if (!success) {
+ return;
+ }
+
+ this.notification.open('Tags updated', 'ok', {
+ duration: 5000
+ });
+ })
+ .catch((error) => {
+ console.error(error);
+
+ this.notification.open('Error updating tags', 'ok', {
+ duration: 5000
+ });
+ }));
+ progbar.track(update);
+ });
+ }
+
+ stopThreadsProgram() {
+ const programData = { name: this.program.name };
+ const dialogRef = this.dialog.open(StopThreadProgramDialogComponent, {
+ data: programData
+ });
+
+ dialogRef.afterClosed().subscribe(result => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ const stopThreads = (this.programService.stopThreadsProgram(this.program.id)
+ .catch(() => { return false; })
+ .then(success => {
+ if (!success) {
+ return;
+ }
+ this.notification.open('All Threads stopped', 'ok', {
+ duration: 5000
+ });
+ }));
+ progbar.track(stopThreads);
+ });
+ }
+
+ async downloadScreenshot() {
+ // See: https://stackoverflow.com/q/23218174
+ const canvas = this.workspace.getPrintViewCanvas();
+ const name = this.program.name.replace(/[^a-zA-Z0-9]/g, '-').replace(/--+/g, '-') + '.svg';
+
+ // Pull style file
+ const styles = document.createElementNS(SvgNS, 'style');
+ styles.innerHTML = ('/* )) +
+ // Supplement flow editor CSS with global styles that affect it
+ '* {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }\n' +
+ '/* ]]> */<');
+
+ canvas.insertBefore(styles, canvas.firstChild);
+
+ // Make image locations absolute
+ for (const image of Array.from(canvas.getElementsByTagNameNS(SvgNS, 'image')) as SVGImageElement[]) {
+ let baseServerPath = document.location.origin;
+
+ if (image.href && image.href.baseVal.startsWith('/')) {
+ // Image relative to current domain
+ image.href.baseVal = baseServerPath + image.href.baseVal;
+ }
+ }
+
+ // Build XML blob
+ const serializer = new XMLSerializer();
+
+ let source = serializer.serializeToString(canvas);
+
+ //add name spaces.
+ if(!source.match(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)){
+ source = source.replace(/^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)){
+ source = source.replace(/^ {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ const deletion = (this.programService.deleteProgramById(this.program.id)
+ .catch(() => { return false; })
+ .then(success => {
+ if (!success) {
+ return;
+ }
+
+ this.goBack();
+ }));
+ progbar.track(deletion);
+ });
+ }
+
+ toggleLogsPanel() {
+ if (this.drawer.opened && this.sidepanel.drawerType === 'logs') {
+ this.closeDrawer();
+ }
+ else {
+ this.sidepanel.setDrawerType('logs');
+ if (!this.drawer.opened) {
+ this.openDrawer();
+ }
+ }
+ }
+
+ toggleVariablesPanel() {
+ if (this.drawer.opened && this.sidepanel.drawerType === 'variables') {
+ this.closeDrawer();
+ }
+ else {
+ this.sidepanel.setDrawerType('variables');
+ if (!this.drawer.opened) {
+ this.openDrawer();
+ }
+ }
+ }
+
+ notifyResize() {
+ this.browser.window.dispatchEvent(new Event('resize'));
+ }
+
+ openDrawer() {
+ return this.drawer.open();
+ }
+
+ closeDrawer = () => {
+ return this.drawer.close();
+ }
+}
diff --git a/frontend/src/app/flow-editor/flow_block.ts b/frontend/src/app/flow-editor/flow_block.ts
new file mode 100644
index 00000000..e40172b1
--- /dev/null
+++ b/frontend/src/app/flow-editor/flow_block.ts
@@ -0,0 +1,137 @@
+import { FlowWorkspace } from "./flow_workspace";
+
+export type MessageType = 'integer' | 'float' | 'boolean' | 'string' | 'any' | 'pulse' | 'user-pulse' | 'list';
+
+export interface Position2D { x: number; y: number };
+export interface Area2D { x: number, y: number, width: number, height: number };
+export interface ManipulableArea2D { left: number, top: number, right: number, bottom: number };
+
+export interface Movement2D { x: number; y: number };
+
+export interface OutputPortDefinition {
+ type: MessageType;
+ name?: string;
+};
+
+export interface PrimitiveTypeInputPortDefinition {
+ type: MessageType;
+ name?: string;
+ required?: boolean;
+};
+
+export interface BridgeEnumInputPortDefinition {
+ type: 'enum';
+ name?: string;
+ enum_name: string;
+ enum_namespace: string;
+ required?: boolean;
+};
+
+export interface BridgeEnumSequenceInputPortDefinition {
+ type: 'enum_sequence';
+ name?: string;
+ enum_sequence: string[];
+ enum_namespace: string;
+ required?: boolean;
+};
+
+export type InputPortDefinition = PrimitiveTypeInputPortDefinition | BridgeEnumInputPortDefinition | BridgeEnumSequenceInputPortDefinition;
+
+export interface ExtraInputDefinition {
+ type: MessageType,
+ quantity: 'any' | { max: number },
+};
+
+export type OnIOSelected = ((block: FlowBlock,
+ type: 'in'|'out',
+ index: number,
+ definition: InputPortDefinition | OutputPortDefinition,
+ port_center: Position2D,
+ ) => void);
+
+export type OnInputsChanged = ((block: FlowBlock,
+ new_number: number,
+ ) => void);
+
+export type OnDropdownExtended = ((block: FlowBlock,
+ slot_id: string,
+ previous_value: string,
+ current_rect: Area2D,
+ update: (new_value: string) => void,
+ ) => void);
+
+export interface FlowBlockOptions {
+ message?: string;
+ title?: string;
+ outputs?: OutputPortDefinition[];
+ inputs?: InputPortDefinition[];
+ extra_inputs?: ExtraInputDefinition;
+ slots?: {[key: string]: string};
+
+ on_io_selected?: OnIOSelected;
+ on_inputs_changed?: OnInputsChanged;
+ on_dropdown_extended?: OnDropdownExtended;
+}
+
+export type Direction2D = 'up' | 'down' | 'left' | 'right';
+export type FlowBlockData = { type: string, value: any, subtype?: string };
+export interface FlowBlockInitOpts {
+ position?: Position2D;
+ block_id?: string;
+ workspace?: FlowWorkspace;
+};
+
+export interface BlockContextAction {
+ title: string,
+ run: () => void,
+};
+
+export interface FlowBlock {
+ updateOptions(blockData: FlowBlockData): void;
+ moveTo(position: Position2D): void;
+ onMove(callback: (pos: Position2D) => void): void;
+ readonly id: string;
+ dispose(): void;
+ render(canvas: SVGElement, initOpts: FlowBlockInitOpts): SVGElement;
+ serialize(): FlowBlockData;
+
+ getBodyElement(): SVGElement;
+ getBodyArea(): Area2D;
+
+ getOffset(): Position2D;
+ moveBy(distance: Position2D): FlowBlock[];
+ endMove(): FlowBlock[];
+
+ onGetFocus(): void;
+ onLoseFocus(): void;
+
+ addConnection(direction: 'in' | 'out', input: number, block: FlowBlock, sourceType: string): boolean;
+ removeConnection(direction: 'in' | 'out', index: number, block: FlowBlock): boolean;
+ getBlockContextActions(): BlockContextAction[];
+
+ getSlots(): {[key: string]: string};
+ getInputs(): InputPortDefinition[];
+ getPositionOfInput(index: number, edge?: boolean): Position2D;
+ getPositionOfOutput(index: number, edge?: boolean): Position2D;
+ getOutputType(index: number): string;
+ getInputType(index: number): string;
+ getOutputRunwayDirection(): Direction2D;
+}
+
+export interface ContainerBlock {
+ update(): void;
+ removeContentBlock(block: FlowBlock): void;
+ addContentBlock(block: FlowBlock): void;
+ isPage: boolean;
+ id: string;
+}
+
+export interface Resizeable {
+ resize: (dimensions: { width: number, height: number }) => void;
+ getBodyArea: () => Area2D;
+}
+
+export interface FlowActuator {
+ onclick(): void;
+ render(div: HTMLDivElement): HTMLElement;
+}
diff --git a/frontend/src/app/flow-editor/flow_connection.ts b/frontend/src/app/flow-editor/flow_connection.ts
new file mode 100644
index 00000000..9bc31a84
--- /dev/null
+++ b/frontend/src/app/flow-editor/flow_connection.ts
@@ -0,0 +1,28 @@
+export interface SourceDefinition {
+ block_id: string;
+ output_index: number;
+}
+
+export interface SinkDefinition {
+ block_id: string;
+ input_index: number;
+}
+
+export type FlowConnectionData = {
+ source: SourceDefinition,
+ sink: SinkDefinition,
+ id: string,
+ type: string | null,
+};
+
+
+export function setConnectionType(connType: string, conn: FlowConnectionData, element: SVGElement) {
+ conn.type = connType;
+
+ let type_class = "unknown_wire";
+ if (connType) {
+ type_class = connType + '_wire';
+ }
+
+ element.setAttributeNS(null, 'class', 'established connection ' + type_class);
+}
diff --git a/frontend/src/app/flow-editor/flow_graph.ts b/frontend/src/app/flow-editor/flow_graph.ts
new file mode 100644
index 00000000..bca45d47
--- /dev/null
+++ b/frontend/src/app/flow-editor/flow_graph.ts
@@ -0,0 +1,118 @@
+import { FlowBlockData, Position2D } from "./flow_block";
+
+export interface FlowGraphEdge {
+ from: {id: string, output_index: number},
+ to: {id: string, input_index: number},
+};
+
+export interface FlowGraphNode {
+ data: FlowBlockData,
+ position?: Position2D,
+ container_id?: string | null,
+};
+
+export interface FlowGraph {
+ nodes: { [key: string]: FlowGraphNode },
+ edges: FlowGraphEdge[],
+};
+
+// Compiled graph
+export interface CompiledConstantArg {
+ type: 'constant',
+ value: any,
+};
+
+export interface CompiledVariableArg {
+ type: 'variable' | 'list',
+ value: any,
+};
+
+export type MonitorExpectedValue = CompiledConstantArg;
+
+export interface CompiledBlockArgMonitorDict {
+ monitor_id: {
+ from_service: string,
+ },
+ monitor_expected_value: MonitorExpectedValue | 'any_value';
+ key: "utc_time" | "utc_date";
+ save_to?: {
+ type: 'variable',
+ value: string,
+ },
+
+ monitored_value?: number,
+}
+
+export interface CompiledBlockArgCallServiceDict {
+ service_id: string,
+ service_action: string,
+ service_call_values: CompiledBlockArgList,
+}
+
+export interface CompiledBlockArgBlock {
+ type: 'block',
+ value: CompiledBlock[]
+}
+
+export interface CompiledBlockServiceCallSelectorArgs {
+ key: string,
+ subkey?: { type: 'argument', index: number },
+};
+
+export type CompiledBlockArg = CompiledBlockArgBlock | CompiledConstantArg | CompiledVariableArg;
+
+export type CompiledBlockArgList = CompiledBlockArg[];
+
+export type CompiledBlockType = "wait_for_monitor"
+ | "control_wait_for_next_value"
+ | "control_if_else" | "control_repeat"
+ | "operator_and" | "operator_equals" | "operator_lt" | "operator_gt"
+ | "operator_add" | "operator_modulo"
+ | "flow_last_value"
+ | "data_setvariableto" | "data_variable"
+ | "command_call_service"
+ | "control_wait"
+ | "logging_add_log" | "flow_get_thread_id"
+ | "jump_to_position"
+ | "jump_to_block"
+ | "op_fork_execution"
+ | "trigger_when_all_completed"
+ | "trigger_when_first_completed"
+ | "op_preload_getter"
+ | "data_lengthoflist" | "data_deleteoflist" | "data_addtolist"
+
+// Ui-related
+ | "data_ui_block_value"
+
+// Signal-only
+ | "on_data_variable_update"
+ | "op_on_block_run"
+
+// Not found on executable stage, will be removed in link phase
+ | "jump_point"
+
+// Operations should not appear on a properly compiled AST
+ | "trigger_when_all_completed" | "trigger_when_first_completed"
+ | "trigger_on_signal" | "trigger_when_all_true"
+ ;
+
+export type CompiledBlockArgs
+ = CompiledBlockArgMonitorDict
+ | CompiledBlockArgCallServiceDict
+ | CompiledBlockArgList
+ | CompiledBlockServiceCallSelectorArgs
+;
+
+export interface ContentBlock {
+ contents: (CompiledBlock | ContentBlock)[],
+};
+
+export interface CompiledBlock {
+ id?: string,
+ type: CompiledBlockType,
+ args?: CompiledBlockArgs,
+ contents?: (CompiledBlock | ContentBlock)[],
+ report_state?: boolean,
+};
+
+export type CompiledFlowGraph = CompiledBlock[];
diff --git a/frontend/src/app/flow-editor/flow_workspace.ts b/frontend/src/app/flow-editor/flow_workspace.ts
new file mode 100644
index 00000000..bb2f856e
--- /dev/null
+++ b/frontend/src/app/flow-editor/flow_workspace.ts
@@ -0,0 +1,3467 @@
+import { AtomicFlowBlock, AtomicFlowBlockData } from './atomic_flow_block';
+import { BlockManager } from './block_manager';
+import { DirectValue } from './direct_value';
+import { EnumDirectValue, EnumGetter, EnumValue } from './enum_direct_value';
+import { Area2D, BridgeEnumInputPortDefinition, ContainerBlock, Direction2D, FlowBlock, FlowBlockData, InputPortDefinition, MessageType, OutputPortDefinition, Position2D, Resizeable, BridgeEnumSequenceInputPortDefinition } from './flow_block';
+import { FlowConnectionData, SourceDefinition, SinkDefinition, setConnectionType } from './flow_connection';
+import { FlowGraph, FlowGraphEdge, FlowGraphNode } from './flow_graph';
+import { Toolbox } from './toolbox';
+import { ContainerFlowBlock, ContainerFlowBlockData, isContainerFlowBlockData } from './ui-blocks/container_flow_block';
+import { UiFlowBlock, UiFlowBlockData } from './ui-blocks/ui_flow_block';
+import { isContainedIn, uuidv4, maxKey } from './utils';
+import { MatDialog } from '@angular/material/dialog';
+import { ConfigureBlockDialogComponent, ConfigurableBlock, BlockConfigurationOptions } from './dialogs/configure-block-dialog/configure-block-dialog.component';
+import { ProgramService } from '../program.service';
+import { CannotSetAsContentsError } from './ui-blocks/cannot_set_as_contents_error';
+import * as Y from 'yjs';
+import { WebsocketProvider } from 'y-websocket'
+import { ProgramEditorEventValue } from 'app/program';
+import { Synchronizer } from 'app/syncronizer';
+import { EnvironmentService } from 'app/environment.service';
+import { SessionService } from 'app/session.service';
+import { ToastrService } from 'ngx-toastr';
+
+///
+declare const Fuse: any;
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+const ABSOLUTE_MAX_ITERATIONS = 100;
+const DEFAULT_MAX_ITERATIONS = 10;
+
+const INV_MAX_ZOOM_LEVEL = 5;
+const TIME_BETWEEN_POSITION_ITERATIONS = 100; // In milliseconds
+
+const CUT_POINT_SEARCH_INCREASES = 10;
+const CUT_POINT_SEARCH_SPACING = CUT_POINT_SEARCH_INCREASES;
+
+const PRINT_MARGIN = 20;
+
+// Draw helper
+const HELPER_PADDING = 10;
+const HELPER_SEPARATION = 40;
+const HELPER_EXTRA_Y = 25;
+
+// Zoom management
+const SMALL_ZOOM_INCREMENTS = 0.1;
+const LARGE_ZOOM_INCREMENTS = 0.25;
+const FAB_BUTTON_PADDING = 5;
+
+type ConnectableNode = {
+ block: FlowBlock,
+ type: 'in' | 'out',
+ index: number,
+};
+
+type State = 'waiting' // Base state
+ | 'dragging-block' // Moving around a block
+ | 'dragging-workspace' // Moving around the workspace
+ | 'selecting-workspace'
+ ;
+
+type SharedBlockData = {
+ connections: string[];
+ container_id: string | null;
+ position: Position2D;
+ blockData: FlowBlockData,
+};
+
+export class IncompatibleConnectionError extends Error {}
+
+export class FlowWorkspace implements BlockManager {
+ private eventStream: Synchronizer;
+ private eventSubscription: any;
+ private cursorDiv: HTMLElement;
+ private cursorInfo: {[key: string]: HTMLElement};
+ private wsSyncProvider: WebsocketProvider;
+
+
+ public static BuildOn(baseElement: HTMLElement,
+ getEnum: EnumGetter,
+ dialog: MatDialog,
+ programId: string,
+ programService: ProgramService,
+ read_only: boolean,
+ sessionService: SessionService,
+ environmentService: EnvironmentService,
+ toastr: ToastrService,
+ ): FlowWorkspace {
+ let workspace: FlowWorkspace;
+ try {
+ workspace = new FlowWorkspace(baseElement, getEnum, dialog, programId,
+ programService, read_only,
+ sessionService, environmentService,
+ toastr);
+ workspace.init();
+ }
+ catch(err) {
+ console.error(err);
+ workspace.dispose();
+
+ throw err;
+ }
+
+ return workspace;
+ }
+
+ public setToolbox(toolbox: Toolbox) {
+ this.toolbox = toolbox;
+ }
+
+ public onResize() {
+ this.update_top_left();
+ }
+
+ public getCanvas(): SVGSVGElement {
+ return this.canvas;
+ }
+
+ public getGraph(): FlowGraph {
+ const blocks: { [key: string]: FlowGraphNode } = {};
+ for (const block_id of Object.keys(this.blockObjs)) {
+ const blockObj = this.blockObjs[block_id].block;
+ const serialized = blockObj.serialize();
+ const position = blockObj.getOffset();
+
+ blocks[block_id] = { data: serialized, position: position, container_id: this.blocks.get(block_id)?.container_id };
+ }
+
+ const connections: FlowGraphEdge[] = [];
+
+ for (const conn_id of Array.from(this.connections.keys())) {
+ const connection = this.connections.get(conn_id);
+
+ const source = connection.source;
+ const sink = connection.sink;
+ connections.push({
+ from: { id: source.block_id, output_index: source.output_index },
+ to: { id: sink.block_id, input_index: sink.input_index },
+ });
+ }
+
+ return {
+ nodes: blocks,
+ edges: connections,
+ }
+ }
+
+ public getPages(): {[key: string]: any} {
+ const pages: { [key: string]: any } = {};
+ for (const block_id of Object.keys(this.blockObjs)) {
+ const block = this.blockObjs[block_id].block;
+ if (block instanceof ContainerFlowBlock) {
+ if (block.isPage) {
+ pages['/'] = { value: block.renderAsUiElement(), title: block.getPageTitle() };
+ }
+ }
+ }
+
+ return pages;
+ }
+
+ public load(graph: FlowGraph) {
+ this.autoposition = false;
+
+ // TODO: Merge with _sortByDependencies?
+ let to_go = Object.keys(graph.nodes);
+
+ let processing = true;
+ let lastProcessing = true;
+
+ while ((to_go.length > 0) /* && processing */) {
+ processing = false;
+ const skipped = [];
+
+ for (const block_id of to_go) {
+ const block = graph.nodes[block_id];
+ if (lastProcessing || processing) {
+ if (block.container_id && (!this.blockObjs[block.container_id])) {
+ skipped.push(block_id);
+ continue;
+ }
+ }
+ else {
+ console.error("Doing an exception to jump over circular dependencies");
+ }
+
+ const created_block = this.deserializeBlock(block_id, block.data);
+
+ if (!created_block) {
+ console.error("Error deserializing block:", block.data);
+ continue;
+ }
+
+ try {
+ this.draw(created_block, block.position);
+ }
+ catch (err) {
+ console.error("Error drawing block", err);
+ continue;
+ }
+
+ if (block.container_id) {
+ try {
+ this._updateBlockContainer(created_block, this.blockObjs[block.container_id].block);
+ }
+ catch (err) {
+ if (err instanceof CannotSetAsContentsError) {
+ this._updateBlockContainer(created_block, null);
+ }
+ else {
+ throw err;
+ }
+ }
+ }
+ processing = true;
+ }
+
+ to_go = skipped;
+ lastProcessing = processing;
+ }
+
+ if (to_go.length !== 0) {
+ throw new Error("Found container-contained circular dependency, on the following IDs: " + JSON.stringify(to_go));
+ }
+
+ for (const conn of graph.edges) {
+ try {
+ this.establishConnection(
+ {
+ block: this.blockObjs[conn.from.id].block,
+ type: 'out',
+ index: conn.from.output_index,
+ },
+ {
+ block: this.blockObjs[conn.to.id].block,
+ type: 'in',
+ index: conn.to.input_index,
+ },
+ )
+ }
+ catch(err) {
+ console.error("Error establishing connection", err);
+ }
+ }
+
+ this._initializeReady();
+ }
+
+ public initializeEmpty() {
+ this._initializeReady();
+ }
+
+ private _initializeReady() {
+ this.autoposition = true;
+
+ this._initializeEventSynchronization();
+ }
+
+ private _initializeEventSynchronization() {
+ // Initialize editor event listeners
+ // This is used for collaborative editing.
+
+ if (this.read_only || !this.environmentService.hasYjsWsSyncServer()) {
+ // We won't have to wait for the last state to get loaded
+ return;
+ }
+
+ this.blocks.observe(this._onBlockChange.bind(this));
+ this.connections.observe(this._onConnectionChange.bind(this));
+
+ // HACK: Give some space to blocks and connections to sync before establishing connection
+ setTimeout(() => {
+ this.wsSyncProvider = new WebsocketProvider(this.environmentService.getYjsWsSyncServer(),
+ this.programId,
+ this.doc,
+ { params: {token: this.sessionService.getToken()} });
+
+ this.eventStream = this.programService.getEventStream(this.programId, { skip_previous: true });
+ this.eventSubscription = this.eventStream.subscribe(
+ {
+ next: (ev: ProgramEditorEventValue) => {
+ if (ev.type === 'cursor_event') {
+ this.drawPointer(ev.value);
+ }
+ else if (ev.type === 'add_editor') {
+ this.newPointer(ev.value.id);
+ }
+ else if (ev.type === 'remove_editor') {
+ this.deletePointer(ev.value.id);
+ }
+ else if (ev.type === 'ready') {
+ // Nothing to do in this editor.
+ }
+ },
+ error: (error: any) => {
+ console.error("Error obtainig editor events:", error);
+ },
+ complete: () => {
+ console.log("Disconnected");
+ }
+ }
+ );
+
+ const onMouseEvent = ((ev: MouseEvent) => {
+ const disp = this.getEditorPosition();
+
+ const rect = this.baseElement.getBoundingClientRect();
+ const cursorInWorkspace = { x: ev.x - rect.left, y: ev.y - rect.top }
+
+ const posInCanvas = {
+ x: cursorInWorkspace.x / disp.scale - disp.x,
+ y: cursorInWorkspace.y / disp.scale - disp.y,
+ }
+
+ this.eventStream.push({ type: 'cursor_event', value: posInCanvas })
+ });
+
+ this.baseElement.onmousemove = onMouseEvent;
+ this.baseElement.onmouseup = onMouseEvent;
+ }, 0);
+ }
+
+ /* Collaborator pointer management */
+ newPointer(id: string): HTMLElement {
+ const cursor = document.createElement('object');
+ cursor.type = 'image/svg+xml';
+ cursor.style.display = 'none';
+ cursor.style.position = 'fixed';
+ cursor.style.height = '2.5ex';
+ cursor.style.color
+ cursor.style.zIndex = '10';
+ cursor.style.pointerEvents = 'none';
+ cursor.data = '/assets/cursor.svg';
+ cursor.onload = () => {
+ // Give the cursor a random color
+ cursor.getSVGDocument().getElementById('cursor').style.fill = `hsl(${Math.random() * 255},50%,50%)`;
+ };
+
+ this.cursorDiv.appendChild(cursor);
+ this.cursorInfo[id] = cursor;
+
+ return cursor;
+ }
+
+ getPointer(id: string): HTMLElement {
+ if (this.cursorInfo[id]) {
+ return this.cursorInfo[id];
+ }
+
+ return this.newPointer(id);
+ }
+
+ deletePointer(id: string) {
+ const cursor = this.cursorInfo[id];
+ if (!cursor) {
+ return;
+ }
+ this.cursorDiv.removeChild(cursor);
+ delete this.cursorInfo[id];
+ }
+
+ drawPointer(pos:{id: string, x : number, y: number}) {
+ const rect = this.baseElement.getBoundingClientRect();
+ const disp = this.getEditorPosition();
+ const cursor = this.getPointer(pos.id);
+
+ const posInScreen = {
+ x: (pos.x + disp.x) * disp.scale + rect.left,
+ y: (pos.y + disp.y) * disp.scale + rect.top,
+ }
+ cursor.style.left = posInScreen.x + 'px';
+ cursor.style.top = posInScreen.y + 'px';
+
+ let inEditor = false;
+ if (rect.left <= posInScreen.x && rect.right >= posInScreen.x) {
+ if (rect.top <= posInScreen.y && rect.bottom >= posInScreen.y) {
+ inEditor = true;
+ }
+ }
+
+ if (inEditor) {
+ cursor.style.display = 'block';
+ }
+ else {
+ cursor.style.display = 'none';
+ }
+ }
+
+ getEditorPosition(): {x:number, y: number, scale: number} | null {
+ return {
+ x: -this.top_left.x,
+ y: -this.top_left.y,
+ scale: 1/this.inv_zoom_level,
+ }
+ }
+
+
+ private deserializeBlock(blockId: string, blockData: FlowBlockData) {
+ switch (blockData.type) {
+ case AtomicFlowBlock.GetBlockType():
+ return AtomicFlowBlock.Deserialize(blockData as AtomicFlowBlockData, blockId, this);
+
+ case UiFlowBlock.GetBlockType():
+ if (isContainerFlowBlockData(blockData)) {
+ return ContainerFlowBlock.Deserialize(blockData as ContainerFlowBlockData, blockId, this, this.toolbox);
+ }
+ else {
+ return UiFlowBlock.Deserialize(blockData as UiFlowBlockData, blockId, this, this.toolbox);
+ }
+
+ case DirectValue.GetBlockType():
+ return DirectValue.Deserialize(blockData, blockId, this);
+
+ case EnumDirectValue.GetBlockType():
+ return EnumDirectValue.Deserialize(blockData, blockId, this, this.getEnum);
+
+ default:
+ console.error("Unknown block type:", blockData.type);
+ }
+ }
+
+ private numPages = 0;
+ private baseElement: HTMLElement;
+ private inlineEditorContainer: HTMLDivElement;
+ private inlineEditor: HTMLInputElement;
+ private inlineMultilineEditor: HTMLTextAreaElement;
+ private state: State = 'waiting';
+ private toolbox: Toolbox;
+ private autoposition: boolean;
+
+ private popupGroup: HTMLDivElement;
+ private canvas: SVGSVGElement;
+ private selectionRect: SVGRectElement;
+
+ private connection_group: SVGGElement;
+ private block_group: SVGGElement;
+ private container_group: SVGGElement;
+ private containers: (FlowBlock & ContainerBlock)[] = [];
+
+ private top_left = { x: 0, y: 0 };
+ private inv_zoom_level = 1;
+ private input_helper_section: SVGGElement;
+ private trashcan: SVGGElement;
+ private button_group: SVGGElement;
+ private variables_in_use: { [key: string]: number } = {};
+ private getEnum: EnumGetter;
+
+ public getInvZoomLevel(): number {
+ return this.inv_zoom_level;
+ }
+
+ private doc: Y.Doc;
+ private blocks: Y.Map;
+ private connections: Y.Map;
+
+ private connectionElements: {[key: string]: SVGElement} = {};
+ private blockObjs: {[key: string]: {
+ block: FlowBlock,
+ input_group: SVGGElement,
+ }} = {};
+
+ private _selectedBlocks: string[] = [];
+
+
+ public getDialog(): MatDialog {
+ return this.dialog;
+ }
+
+ private constructor(baseElement: HTMLElement,
+ getEnum: EnumGetter,
+ private dialog: MatDialog,
+ private programId: string,
+ private programService: ProgramService,
+ private read_only: boolean,
+ private sessionService: SessionService,
+ private environmentService: EnvironmentService,
+ private toastr: ToastrService,
+ ) {
+ this.baseElement = baseElement;
+ this.getEnum = getEnum;
+
+ this.doc = new Y.Doc();
+
+ this.blocks = this.doc.getMap('blocks');
+ this.connections = this.doc.getMap('connections');
+ }
+
+ private _containerDependencies: {[ key: string ]: [string, string][] } = {};
+ private _connectionDependencies: {[ key: string ]: FlowConnectionData[] } = {};
+
+ private _onBlockChange(event: Y.YMapEvent, _transaction: Y.Transaction) {
+ const moves = [] as string[];
+ event.changes.keys.forEach((change, key) => {
+ if (change.action === 'add') {
+ const info = this.blocks.get(key);
+ console.info(`BLOCK "${key}" was added. Initial value:`, info);
+
+ if (key in this.blockObjs) {
+ console.log("Already contained");
+ return;
+ }
+
+ const block = this.deserializeBlock(key, info.blockData);
+ this.draw(block, info.position);
+
+ moves.push(key);
+
+ const containerId = info.container_id;
+ if ((block instanceof UiFlowBlock) && containerId) {
+
+ if (!(containerId in this.blockObjs)) {
+ // Not ready to put this on container
+ if (!this._containerDependencies[containerId]) {
+ this._containerDependencies[containerId] = [];
+ }
+ this._containerDependencies[containerId].push([key, null]);
+ }
+ else {
+ this._updateBlockContainerFromContainer(block,
+ this.blockObjs[containerId].block,
+ null);
+ }
+ }
+
+ for (const [dependent, old] of this._containerDependencies[key] || []) {
+ const depBlock = this.blockObjs[dependent].block;
+
+ this._updateBlockContainerFromContainer(depBlock, block, this.blockObjs[old]?.block);
+ }
+
+ for (const conn of this._connectionDependencies[key] || []) {
+ if ((conn.source.block_id in this.blockObjs) && (conn.sink.block_id in this.blockObjs)) {
+ console.log("Ready for", conn);
+
+ this.addConnection(conn.source, conn.sink);
+ }
+ else {
+ console.log("Waiting for other section for", conn);
+ }
+ }
+
+
+ this._raiseBlocks([key]);
+ delete this._containerDependencies[key];
+ delete this._connectionDependencies[key];
+ }
+ else if (change.action === 'update') {
+ // console.debug(`BLOCK "${key}" was updated.`);
+
+ // Note that moveTo() does not trigger `block.onMove()` callbacks.
+ const block = this.blockObjs[key].block;
+ const newData = this.blocks.get(key);
+ block.moveTo(newData.position);
+ this._afterBlocksMove([key]);
+
+ const containerId = newData.container_id;
+
+ if (block instanceof UiFlowBlock) {
+ if (containerId !== change.oldValue.container_id) {
+ if (containerId && !(containerId in this.blockObjs)) {
+ // Not ready to put this on container
+ if (!this._containerDependencies[containerId]) {
+ this._containerDependencies[containerId] = [];
+ }
+ this._containerDependencies[containerId].push([key, change.oldValue.container_id]);
+ }
+ else {
+ this._updateBlockContainerFromContainer(block,
+ this.blockObjs[containerId]?.block,
+ this.blockObjs[change.oldValue.container_id]?.block);
+ }
+ }
+ }
+
+ // Updated block data
+ const updatedOptions = JSON.stringify(newData.blockData) === JSON.stringify(change.oldValue.blockData);
+ block.updateOptions(newData.blockData);
+ }
+ else if (change.action === 'delete') {
+ console.info(`Property "${key}" was deleted. New value: undefined. Previous value: "${change.oldValue}".`)
+
+ if (!(key in this.blockObjs)) {
+ console.log("Already deleted");
+ return;
+ }
+
+ const block = this.blockObjs[key].block;
+
+ if (block instanceof UiFlowBlock) {
+ this._updateBlockContainerFromContainer(block, null, this.blockObjs[change.oldValue.container_id]?.block);
+ }
+
+ this.removeBlock(key, change.oldValue);
+
+ }
+ });
+ this._afterBlocksMove(moves)
+ }
+
+ private _onConnectionChange(event: Y.YMapEvent, _transaction: Y.Transaction) {
+ event.changes.keys.forEach((change, key) => {
+ if (change.action === 'add') {
+ console.info(`CONNECTION "${key}" added. Initial value:`, this.connections.get(key));
+
+ const conn: FlowConnectionData = this.connections.get(key);
+
+ if ((!(conn.source.block_id in this.blockObjs)) || (!(conn.sink.block_id in this.blockObjs))) {
+ console.log("Reserving CONN to", conn.source.block_id, conn.sink.block_id);
+ if (!(conn.source.block_id in this._connectionDependencies)) {
+ this._connectionDependencies[conn.source.block_id] = [];
+ }
+ if (!(conn.sink.block_id in this._connectionDependencies)) {
+ this._connectionDependencies[conn.sink.block_id] = [];
+ }
+
+ this._connectionDependencies[conn.source.block_id].push(conn);
+ this._connectionDependencies[conn.sink.block_id].push(conn);
+ }
+ else {
+ this.addConnection(conn.source, conn.sink);
+ }
+ }
+ else if (change.action === 'update') {
+ // console.debug(`CONNECTION "${key}" updated. New value:`, this.connections.get(key),
+ // '. Previous value:', change.oldValue);
+ // As this mostly immutable, this signal isn't really useful
+ }
+ else if (change.action === 'delete') {
+ console.info(`CONNECTION "${key}" removed.`);
+ if (key in this.connectionElements) {
+ this.removeConnection(change.oldValue);
+ }
+ else {
+ console.debug(`Attempting to remove inexisting connection: ${key}.`);
+ }
+ }
+ })
+ }
+
+ public onBlockOptionsChanged(block: FlowBlock) {
+ const serialized = block.serialize();
+ const saveData = this.blocks.get(block.id);
+ saveData.blockData = serialized;
+ this.blocks.set(block.id, saveData);
+ }
+
+ private init() {
+ // Inline editor
+ this.inlineEditorContainer = document.createElement('div');
+ this.inlineEditorContainer.setAttribute('class', 'inline_editor_container hidden');
+ this.baseElement.appendChild(this.inlineEditorContainer);
+ this.inlineEditor = document.createElement('input');
+ this.inlineEditorContainer.appendChild(this.inlineEditor);
+ this.inlineMultilineEditor = document.createElement('textarea');
+ this.inlineEditorContainer.appendChild(this.inlineMultilineEditor);
+
+ // Popup group
+ this.popupGroup = document.createElement('div');
+ this.popupGroup.setAttribute('class', 'popup_group hidden');
+ this.baseElement.appendChild(this.popupGroup);
+
+ this.canvas = document.createElementNS(SvgNS, "svg");
+ this.canvas.setAttribute('class', 'block_renderer ' + (this.read_only ? "read-only" : ''));
+
+ this.selectionRect = document.createElementNS(SvgNS, "rect");
+ this.selectionRect.setAttribute('class', 'selection');
+
+ this.input_helper_section = document.createElementNS(SvgNS, "g");
+ this.trashcan = document.createElementNS(SvgNS, "g");
+ this.button_group = document.createElementNS(SvgNS, "g");
+
+ this.connection_group = document.createElementNS(SvgNS, "g");
+ this.block_group = document.createElementNS(SvgNS, 'g');
+ this.container_group = document.createElementNS(SvgNS, 'g');
+
+ // The order of elements determines the relative Z-index
+ // The "later" an element is added, the "higher" it is.
+ // The elements are stored in groups so their Z-indexes are consistent.
+ this.canvas.appendChild(this.container_group);
+ this.canvas.appendChild(this.input_helper_section);
+ this.canvas.appendChild(this.trashcan);
+
+ this.canvas.appendChild(this.connection_group);
+ this.canvas.appendChild(this.block_group);
+ this.canvas.appendChild(this.button_group);
+ this.canvas.appendChild(this.selectionRect);
+
+ this.baseElement.appendChild(this.canvas);
+
+ this.init_definitions();
+ this.set_events();
+ this.init_trashcan();
+ this.init_buttons();
+ this.init_cursors();
+
+ this.update_top_left();
+ }
+
+ private init_definitions() {
+ const pulse_head_color = "#ffcc00";
+ const pulse_head_selected_color = "#bf8c00";
+
+ const definitions = document.createElementNS(SvgNS, 'defs');
+ definitions.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+ this.connection_group.appendChild(definitions);
+ }
+
+ private init_trashcan() {
+ this.trashcan.setAttribute('class', 'trashcan helper ' + (this.read_only ? 'invisible' : '') );
+
+ const rect = document.createElementNS(SvgNS, 'rect');
+ rect.setAttributeNS(null, 'class', 'backdrop');
+ rect.setAttributeNS(null, 'width', '5ex');
+ rect.setAttributeNS(null, 'height', '8ex');
+
+ const shadow = document.createElementNS(SvgNS, 'rect');
+ shadow.setAttributeNS(null, 'class', 'backdrop_shadow');
+ shadow.setAttributeNS(null, 'width', '5ex');
+ shadow.setAttributeNS(null, 'height', '8ex');
+
+ const image = document.createElementNS(SvgNS, 'image');
+ image.setAttributeNS(null, 'href', '/assets/sprites/delete_forever-black.svg');
+ image.setAttributeNS(null, 'width', '5ex');
+ image.setAttributeNS(null, 'height', '8ex');
+
+ this.trashcan.appendChild(shadow);
+ this.trashcan.appendChild(rect);
+ this.trashcan.appendChild(image);
+ }
+
+ private init_buttons() {
+ this.button_group.setAttribute('class', 'fab-button-group');
+ let button_size = null;
+
+ {
+ const zoom_in_button = document.createElementNS(SvgNS, 'g');
+ zoom_in_button.setAttribute('class', 'button');
+ zoom_in_button.onclick = () => { this.zoom_in(); }
+ const shadow = document.createElementNS(SvgNS, 'circle');
+ shadow.setAttributeNS(null, 'class', 'button-shadow');
+ shadow.setAttributeNS(null, 'r', '2ex');
+
+ const body = document.createElementNS(SvgNS, 'circle');
+ body.setAttributeNS(null, 'class', 'button-body');
+ body.setAttributeNS(null, 'r', '2ex');
+
+ const symbol = document.createElementNS(SvgNS, 'path');
+ symbol.setAttributeNS(null, 'class', 'button-symbol');
+ symbol.setAttributeNS(null, 'd', 'M-10,0 h20 m-10,-10 v20'); // A `+` sign
+
+ zoom_in_button.appendChild(shadow);
+ zoom_in_button.appendChild(body);
+ zoom_in_button.appendChild(symbol);
+ this.button_group.appendChild(zoom_in_button);
+
+ button_size = zoom_in_button.getBBox();
+ }
+
+ {
+ const zoom_out_button = document.createElementNS(SvgNS, 'g');
+ zoom_out_button.setAttribute('class', 'button');
+ zoom_out_button.setAttribute('transform', `translate(0, ${ button_size.height + FAB_BUTTON_PADDING * 2 })`);
+ zoom_out_button.onclick = () => { this.zoom_out(); }
+ const shadow = document.createElementNS(SvgNS, 'circle');
+ shadow.setAttributeNS(null, 'class', 'button-shadow');
+ shadow.setAttributeNS(null, 'r', '2ex');
+
+ const body = document.createElementNS(SvgNS, 'circle');
+ body.setAttributeNS(null, 'class', 'button-body');
+ body.setAttributeNS(null, 'r', '2ex');
+
+ const symbol = document.createElementNS(SvgNS, 'path');
+ symbol.setAttributeNS(null, 'class', 'button-symbol');
+ symbol.setAttributeNS(null, 'd', 'M-10,0 h20'); // A `-` sign
+
+ zoom_out_button.appendChild(shadow);
+ zoom_out_button.appendChild(body);
+ zoom_out_button.appendChild(symbol);
+ this.button_group.appendChild(zoom_out_button);
+ }
+
+ {
+ const zoom_reset_button = document.createElementNS(SvgNS, 'g');
+ zoom_reset_button.setAttribute('class', 'button');
+ zoom_reset_button.setAttribute('transform', `translate(0, ${ (button_size.height + FAB_BUTTON_PADDING * 2) * 2 })`);
+ zoom_reset_button.onclick = () => { this.zoom_reset(); }
+ const shadow = document.createElementNS(SvgNS, 'circle');
+ shadow.setAttributeNS(null, 'class', 'button-shadow');
+ shadow.setAttributeNS(null, 'r', '2ex');
+
+ const body = document.createElementNS(SvgNS, 'circle');
+ body.setAttributeNS(null, 'class', 'button-body');
+ body.setAttributeNS(null, 'r', '2ex');
+
+ const symbol = document.createElementNS(SvgNS, 'path');
+ symbol.setAttributeNS(null, 'class', 'button-symbol');
+ symbol.setAttributeNS(null, 'd', 'M-10,-5 h20 m-20,10 h20'); // An `=` sign
+
+ zoom_reset_button.appendChild(shadow);
+ zoom_reset_button.appendChild(body);
+ zoom_reset_button.appendChild(symbol);
+ this.button_group.appendChild(zoom_reset_button);
+ }
+ }
+
+ private init_cursors() {
+ this.cursorDiv = document.getElementById('program-cursors');
+ this.cursorInfo = {};
+ }
+
+ private set_events() {
+ let lastMouseDownTime: null | Date = null;
+ const startMove: (ev?: MouseEvent) => (() => void) = ((ev: MouseEvent | undefined) => {
+ let last = ev ? { x: ev.x, y: ev.y } : null;
+
+ this.state = 'dragging-workspace';
+ this.canvas.classList.add('dragging');
+
+ this.canvas.onmousemove = ((ev: MouseEvent) => {
+ if (last) {
+ this.top_left.x -= (ev.x - last.x) * this.inv_zoom_level;
+ this.top_left.y -= (ev.y - last.y) * this.inv_zoom_level;
+ }
+ last = { x: ev.x, y: ev.y };
+
+ this.update_top_left();
+ });
+
+ return () => {
+ this.state = 'waiting';
+ this.canvas.classList.remove('dragging');
+ this.canvas.onmousemove = null;
+ }
+ });
+
+ this.canvas.onmousedown = (ev: MouseEvent) => {
+ if (this.state !== 'waiting') {
+ return;
+ }
+
+ this.ensureContextMenuHidden();
+
+ const time = new Date();
+ if (!this.read_only && lastMouseDownTime && (((time as any) - (lastMouseDownTime as any)) < 1000)) {
+ const start = this._getPositionFromEvent(ev);
+ this.state = 'selecting-workspace';
+ this.canvas.classList.add('selecting');
+ this._updateSelectionRectangle(start, start);
+
+ this.canvas.onmousemove = ((ev: MouseEvent) => {
+ this._updateSelectionRectangle(start, this._getPositionFromEvent(ev));
+ });
+
+ this.canvas.onmouseup = (() => {
+ // TODO: Do something with the selection
+ this.state = 'waiting';
+ this.canvas.classList.remove('selecting');
+ this.canvas.onmousemove = null;
+ this.canvas.onmouseup = null;
+ });
+ }
+ else {
+ lastMouseDownTime = time;
+
+ const stopMove = startMove(ev);
+ this.canvas.onmouseup = (() => {
+ this.canvas.onmouseup = null;
+ stopMove();
+ });
+ }
+ };
+
+ // Capture key presses directed to canvas
+ document.body.onkeydown = ((ev: KeyboardEvent) => {
+ if (this.state !== 'waiting') {
+ return;
+ }
+
+ if ((ev.target === document.body) && (ev.code === 'Space')) {
+ const stopMove = startMove(null);
+
+ document.body.onkeyup = ((ev: KeyboardEvent) => {
+ if (ev.code === 'Space') {
+ document.body.onkeyup = null;
+ stopMove();
+ }
+ });
+
+ }
+ });
+
+ this.canvas.ontouchstart = ((ev: TouchEvent) => {
+ if (this.state !== 'waiting') {
+ return;
+ }
+
+ if (ev.target !== this.canvas) {
+ return;
+ }
+
+ lastMouseDownTime = new Date();
+ // TODO: Implement select mode
+
+ const touch = ev.targetTouches[0];
+ let last = { x: touch.clientX, y: touch.clientY };
+
+ this.state = 'dragging-workspace';
+ this.canvas.classList.add('dragging');
+
+ this.canvas.ontouchmove = ((ev: TouchEvent) => {
+ if (ev.targetTouches.length == 0) {
+ return;
+ }
+ if (ev.targetTouches.length == 1) {
+ const touch = ev.targetTouches[0];
+ this.top_left.x -= (touch.clientX - last.x) * this.inv_zoom_level;
+ this.top_left.y -= (touch.clientY - last.y) * this.inv_zoom_level;
+ last = { x: touch.clientX, y: touch.clientY };
+
+ this.update_top_left();
+ }
+ else {
+ console.error("Unexpected action with more than one touch", ev);
+ }
+ });
+
+ this.canvas.ontouchend = (() => {
+ this.state = 'waiting';
+ this.canvas.classList.remove('dragging');
+ this.canvas.ontouchmove = null;
+ this.canvas.ontouchend = null;
+ });
+ })
+
+ this.canvas.onwheel = ((ev) => {
+ if(!ev.deltaY){
+ return; // ???
+ }
+
+ ev.preventDefault();
+ if (ev.deltaY < 0) { // Scroll "up"
+ this.zoom_in();
+ }
+ else {
+ this.zoom_out();
+ }
+ });
+ }
+
+ private _updateSelectionRectangle(origin: Position2D, edge: Position2D) {
+ const topLeft = { x: Math.min(origin.x, edge.x), y: Math.min(origin.y, edge.y) };
+ const botRight = { x: Math.max(origin.x, edge.x), y: Math.max(origin.y, edge.y) };
+ const area = this.absPosToWorkspace({
+ x: topLeft.x,
+ y: topLeft.y,
+ width: botRight.x - topLeft.x,
+ height: botRight.y - topLeft.y
+ });
+
+ this.selectionRect.setAttributeNS(null, 'x', area.x + '');
+ this.selectionRect.setAttributeNS(null, 'y', area.y + '');
+ this.selectionRect.setAttributeNS(null, 'width', area.width + '');
+ this.selectionRect.setAttributeNS(null, 'height', area.height + '');
+
+ const blocks = this._getBlocksInArea(area);
+
+ // Discard blocks that cannot be selected
+ const selectableBlocks = blocks.filter(b => {
+ const block = this.blockObjs[b].block;
+
+ return !((block instanceof ContainerFlowBlock) && (block.isPage));
+ });
+ this.updateSelectBlockList(selectableBlocks);
+ }
+
+ private _getBlocksInArea(area: Area2D): string[] {
+ const blocks = [];
+
+ for (const blockId of Object.keys(this.blockObjs)) {
+ const blockArea = this.blockObjs[blockId].block.getBodyArea();
+ if (isContainedIn(blockArea, area)) {
+ blocks.push(blockId);
+ }
+ }
+
+ return blocks;
+ }
+
+ private updateSelectBlockList(blockIds: string[]) {
+ // Find blocks that are added and removed from the selection
+ const added = [];
+ const removed = [];
+ for (const blockId of blockIds) {
+ if (this._selectedBlocks.indexOf(blockId) < 0) {
+ added.push(blockId);
+ }
+ }
+
+ for (const blockId of this._selectedBlocks) {
+ if (blockIds.indexOf(blockId) < 0) {
+ removed.push(blockId);
+ }
+ }
+
+ // Update blocks style
+ added.forEach(blockId => {
+ const block = this.blockObjs[blockId].block;
+ block.getBodyElement().classList.add('selected');
+ block.onGetFocus();
+ })
+ removed.forEach(blockId => {
+ const blockObj = this.blockObjs[blockId];
+ if (blockObj) {
+ blockObj.block.onLoseFocus();
+ blockObj.block.getBodyElement().classList.remove('selected');
+ }
+ else {
+ console.error(`Error unselecting block (id: ${blockId}). Block not found.`);
+ }
+ })
+
+ this._selectedBlocks = blockIds.concat([]); // Clone the list, just for safety
+ this._raiseSelectedBlocks();
+ }
+
+ private _raiseSelectedBlocks() {
+ this._raiseBlocks(this._selectedBlocks);
+ }
+
+ private _raiseBlocks(blockIds: string[]) {
+ const allBlocksUnder = this._getAllBlocksContainedInGroup(blockIds);
+ const sortedBlocks = this._sortByDependencies(allBlocksUnder);
+
+ for (const id of sortedBlocks) {
+ const block = this.blockObjs[id].block;
+ const element = block.getBodyElement();
+
+ element.parentNode.appendChild(element);
+ }
+ }
+
+ private _getAllBlocksContainedInGroup(blockIds: string[]): string[] {
+ // From a list of blocks, add to it all the blocks contained in its
+ // Container blocks.
+
+ const allKnown: {[key: string]: boolean} = {}; // Avoid duplicated results
+ for (const id of blockIds) {
+ if (allKnown[id]) {
+ // Already explored branch
+ continue;
+ }
+
+ allKnown[id] = true;
+ const blockObj = this.blockObjs[id];
+
+ if (blockObj.block instanceof ContainerFlowBlock) {
+
+ for (const content of blockObj.block.recursiveGetAllContents()) {
+ const contentId = content.id;
+ allKnown[contentId] = true;
+ }
+ }
+ }
+
+ return Object.keys(allKnown);
+ }
+
+ private _sortByDependencies(blockIds: string[]): string[] {
+ let to_go = blockIds.concat([]).sort(); // First sort alphabetically, to stabilize the result
+ const sortedByDep = [];
+
+ const processedById: {[key: string]: boolean } = {};
+ for (const blockId of blockIds) {
+ // This is used to differenciate between dependencies not yet
+ // processed or not to be sorted.
+ processedById[blockId] = false;
+ }
+
+ let processing = true;
+
+ while ((to_go.length > 0) && processing) {
+ processing = false;
+ const skipped = [];
+
+ for (const blockId of to_go) {
+ const block = this.blocks.get(blockId);
+
+ if (block.container_id) {
+ // Note that we are not interested on dependencies not in
+ // the move. WE HAVE TO CHECK FOR `FALSE`, NOT FOR EXISTENCE
+ if (processedById[block.container_id] === false) {
+ skipped.push(blockId);
+ continue;
+ }
+ }
+
+ sortedByDep.push(blockId);
+ processedById[blockId] = true;
+ processing = true;
+ }
+
+ to_go = skipped;
+ }
+
+ if (to_go.length !== 0) {
+ throw new Error("Found container-contained circular dependency, on the following IDs: " + JSON.stringify(to_go));
+ }
+
+ return sortedByDep;
+ }
+
+ private ensureBlockSelected(blockId: string) {
+ if (this._selectedBlocks.indexOf(blockId) >= 0) {
+ // It's already selected, nothing to do
+ return;
+ }
+ else {
+ // Update selection
+ this.updateSelectBlockList([blockId]);
+ }
+ }
+
+ private update_top_left() {
+ const width = this.baseElement.clientWidth;
+ const height = this.baseElement.clientHeight;
+
+ this.canvas.setAttributeNS(null, 'viewBox',
+ `${this.top_left.x} ${this.top_left.y} ${width * this.inv_zoom_level} ${height * this.inv_zoom_level}`);
+
+ this.canvas.style.backgroundPosition = `${-this.top_left.x / this.inv_zoom_level}px ${-this.top_left.y / this.inv_zoom_level}px`;
+
+ // Move trashcan
+ const trashbox = this.trashcan.getElementsByTagName('image')[0].getBBox();
+ const trashbox_bottom_margin = 30;
+ if (trashbox) {
+ // Move
+ const left = width * this.inv_zoom_level - trashbox.width * this.inv_zoom_level + this.top_left.x;
+ const top = height * this.inv_zoom_level - trashbox.height * this.inv_zoom_level + this.top_left.y - trashbox_bottom_margin;
+
+ this.trashcan.setAttributeNS(null, 'transform', `matrix(${this.inv_zoom_level},0,0,${this.inv_zoom_level},${left},${top})`);
+
+ // Move buttons
+ {
+ const buttonBox = this.button_group.getBBox();
+
+ const left = width * this.inv_zoom_level - (buttonBox.width - FAB_BUTTON_PADDING) * this.inv_zoom_level + this.top_left.x;
+ const top = height * this.inv_zoom_level - (trashbox.height + FAB_BUTTON_PADDING + buttonBox.height) * this.inv_zoom_level + this.top_left.y - trashbox_bottom_margin;
+
+ this.button_group.setAttributeNS(null, 'transform', `matrix(${this.inv_zoom_level},0,0,${this.inv_zoom_level},${left},${top})`);
+ }
+ }
+ }
+
+ public getPrintViewCanvas(): SVGSVGElement {
+ try {
+ this.hideControls();
+
+ const clone = this.canvas.cloneNode(true) as SVGSVGElement;
+
+ // Find area to cover
+ const blockIds = Object.keys(this.blockObjs);
+ if (blockIds.length === 0) {
+ return clone; // Nothing to show ¯\_(ツ)_/¯
+ }
+
+ const block1Area = this.blockObjs[blockIds[0]].block.getBodyArea();
+ const rect = {
+ left: block1Area.x,
+ top: block1Area.y,
+ right: block1Area.x + block1Area.width,
+ bottom: block1Area.y + block1Area.height,
+ };
+
+ for (let i = 1 ; i < blockIds.length; i++) {
+ const blockArea = this.blockObjs[blockIds[i]].block.getBodyArea();
+
+ if (blockArea.x < rect.left) {
+ rect.left = blockArea.x;
+ }
+ if (blockArea.y < rect.top) {
+ rect.top = blockArea.y;
+ }
+
+ const right = blockArea.x + blockArea.width;
+ const bottom = blockArea.y + blockArea.height;
+ if (right > rect.right) {
+ rect.right = right;
+ }
+ if (bottom > rect.bottom) {
+ rect.bottom = bottom;
+ }
+ }
+
+ const width = rect.right - rect.left;
+ const height = rect.bottom - rect.top;
+
+ clone.setAttributeNS(null, 'viewBox',
+ `${rect.left - PRINT_MARGIN} ${rect.top - PRINT_MARGIN} ${width + PRINT_MARGIN} ${height + PRINT_MARGIN}`);
+
+ // De-select elements
+ for (const selected of Array.from(clone.getElementsByClassName('selected')) as SVGElement[]) {
+ selected.classList.remove('selected');
+ }
+
+ // Remove manipulators
+ for (const manipulator of Array.from(clone.getElementsByClassName('manipulators')) as SVGGElement[]) {
+ manipulator.parentNode.removeChild(manipulator);
+ }
+
+ return clone;
+ }
+ finally {
+ this.showControls();
+ }
+ }
+
+ private hideControls() {
+ this.trashcan.style.visibility = 'hidden';
+ this.button_group.style.visibility = 'hidden';
+ }
+
+ private showControls() {
+ if (!this.read_only) {
+ this.trashcan.style.visibility = 'visible';
+ }
+ this.button_group.style.visibility = 'visible';
+ }
+
+ // Max zoom: 0.5
+ // Min zoom: 1/10
+ // It's easier to manage zoom level with inverses.
+ private zoom_in() {
+ if (this.inv_zoom_level <= 0.5) {
+ this.inv_zoom_level = 0.5;
+ return ;
+ }
+ else if (this.inv_zoom_level > INV_MAX_ZOOM_LEVEL) {
+ this.inv_zoom_level = INV_MAX_ZOOM_LEVEL;
+ }
+ else if (this.inv_zoom_level <= 1) {
+ this.inv_zoom_level -= SMALL_ZOOM_INCREMENTS;
+ }
+ else {
+ this.inv_zoom_level -= LARGE_ZOOM_INCREMENTS;
+ }
+
+ this.update_top_left();
+ }
+
+ private zoom_out() {
+ if (this.inv_zoom_level >= INV_MAX_ZOOM_LEVEL) {
+ this.inv_zoom_level = INV_MAX_ZOOM_LEVEL;
+ return;
+ }
+ else if (this.inv_zoom_level < 0.5) {
+ this.inv_zoom_level = 0.5;
+ }
+ else if (this.inv_zoom_level < 1) {
+ this.inv_zoom_level += SMALL_ZOOM_INCREMENTS;
+ }
+ else {
+ this.inv_zoom_level += LARGE_ZOOM_INCREMENTS;
+ }
+
+ this.update_top_left();
+ }
+
+ private zoom_reset() {
+ this.inv_zoom_level = 1;
+ this.update_top_left(); // This has to be done before center() for it to work correctly
+
+ this.center();
+ }
+
+ // Perform an operation while resetting the zoom level
+ private _withNoZoom(f: () => void) {
+ // Remove zoom
+ const zoomLevel = this.inv_zoom_level;
+ this.inv_zoom_level = 1;
+ this.update_top_left();
+
+ // Apply operation
+ let error = null;
+ let hadException = false;
+
+ try {
+ f();
+ }
+ catch (err) {
+ error = err;
+ hadException = true;
+ }
+
+ // Reset zoo
+ this.inv_zoom_level = zoomLevel;
+ this.update_top_left();
+
+ // Re-throw exception if one was found
+ if (hadException) {
+ throw error;
+ }
+ }
+
+ public dispose() {
+ if (this.inlineEditorContainer) {
+ this.baseElement.removeChild(this.inlineEditorContainer);
+ this.inlineEditorContainer = null;
+ }
+
+ if (this.eventSubscription) {
+ this.eventSubscription.unsubscribe();
+ this.eventSubscription = null;
+ }
+
+ if (this.wsSyncProvider) {
+ this.wsSyncProvider.disconnect();
+ this.wsSyncProvider = null;
+ }
+
+ if (this.popupGroup) {
+ this.baseElement.removeChild(this.popupGroup);
+ this.popupGroup = null;
+ }
+
+ if (this.canvas) {
+ this.baseElement.removeChild(this.canvas);
+ this.canvas = null;
+ }
+ }
+
+ public getBlock(blockId: string): FlowBlock {
+ if (!this.blockObjs[blockId]) {
+ throw Error(`Block (id=${blockId}) not found`);
+ }
+
+ return this.blockObjs[blockId].block;
+ }
+
+ public drawAbsolute(block: FlowBlock, abs_position: Position2D): string {
+ const canvas_area = this.canvas.getBoundingClientRect();
+
+ const rel_pos = {
+ x: (abs_position.x - canvas_area.x) * this.inv_zoom_level + this.top_left.x,
+ y: (abs_position.y - canvas_area.y) * this.inv_zoom_level + this.top_left.y,
+ };
+
+ return this.draw(block, rel_pos);
+ }
+
+ public draw(block: FlowBlock, position?: Position2D): string {
+ const slots = block.getSlots();
+ if (slots.variable) {
+ if (!this.variables_in_use[slots.variable]) {
+ this.variables_in_use[slots.variable] = 0;
+ }
+ this.variables_in_use[slots.variable]++;
+ }
+
+ let group = this.block_group;
+ const isContainer = block instanceof ContainerFlowBlock;
+ if (isContainer) {
+ group = this.container_group;
+ }
+
+ if (!position) {
+ position = {x: 10, y: 10};
+ }
+
+ this._withNoZoom(() => {
+ block.render(group, {
+ position: position,
+ workspace: this,
+ });
+ });
+
+ if (isContainer) {
+ // Obtaining the area has to be done AFTER the rendering
+ this.containers.push((block as FlowBlock & ContainerBlock));
+
+ if ((block as ContainerFlowBlock).isPage) {
+ this.numPages++;
+ }
+ }
+
+ const bodyElement = block.getBodyElement();
+
+ // Events are set even on read-only contexts, so in later iterations,
+ // that property can be changed dynamically.
+ bodyElement.oncontextmenu = (ev: MouseEvent) => {
+ ev.preventDefault();
+
+ if (this.read_only) {
+ return;
+ }
+
+ this.showBlockContextMenu(this._getPositionFromEvent(ev));
+ };
+
+ let canBeMoved = true;
+ if (block instanceof ContainerFlowBlock) {
+ canBeMoved = !block.cannotBeMoved;
+ }
+
+ if (canBeMoved) {
+ bodyElement.onmousedown = bodyElement.ontouchstart = ((ev: MouseEvent | TouchEvent) => {
+ if (this.state !== 'waiting'){
+ return;
+ }
+
+ if (this.read_only) {
+ return;
+ }
+
+ if (this.current_io_selected) { return; }
+
+ this.ensureContextMenuHidden();
+
+ if ((ev as MouseEvent).button === 2) {
+ // On right click just make sure it is selected, the context
+ // menu will be handled by 'oncontextmenu'.
+ this.ensureBlockSelected(block.id);
+ // TODO: How to perform this action on touch event? Long touch?
+ }
+ else {
+ this._mouseDownOnBlock(this._getPositionFromEvent(ev), block);
+ }
+ });
+ }
+
+ const input_group = this.drawBlockInputHelpers(block);
+
+ this.blockObjs[block.id] = { block: block, input_group: input_group };
+ this.blocks.set(block.id, { connections: [],
+ container_id: null,
+ position: position,
+ blockData: block.serialize(),
+ })
+ block.onMove((pos: Position2D) => {
+ const data = this.blocks.get(block.id);
+ if (!data) {
+ console.warn("Calling block.onMove() after deleted.")
+ return;
+ }
+ data.position = pos;
+ this.blocks.set(block.id, data);
+ })
+
+ return block.id;
+ }
+
+ public centerOnBlock(blockId: string) {
+ const block = this.blockObjs[blockId].block;
+ const area = block.getBodyArea();
+ const centerX = area.x + area.width / 2;
+ const centerY = area.y + area.height / 2;
+
+ this.centerOnPoint({ x: centerX, y: centerY });
+ }
+
+ public centerOnPoint(pos: Position2D) {
+ // Consider toolbox overlap
+ let marginRight = 0;
+ if (this.toolbox.blockShowcase){
+ marginRight = this.toolbox.blockShowcase.getBoundingClientRect().width;
+ }
+
+ const width = this.canvas.width.baseVal.value - marginRight;
+ const height = this.canvas.height.baseVal.value;
+
+ this.top_left.x = (pos.x - width/2) - marginRight;
+ this.top_left.y = pos.y - height/2;
+ this.update_top_left();
+ }
+
+ public center() {
+ // Find the center of all blocks, and center the view there
+ const blockIds = Object.keys(this.blockObjs);
+ if (blockIds.length === 0) {
+ return this.centerOnPoint({ x: 0, y: 0 });
+ }
+
+ const block1Area = this.blockObjs[blockIds[0]].block.getBodyArea();
+ const rect = {
+ left: block1Area.x,
+ top: block1Area.y,
+ right: block1Area.x + block1Area.width,
+ bottom: block1Area.y + block1Area.height,
+ };
+
+ for (let i = 1 ; i < blockIds.length; i++) {
+ const blockArea = this.blockObjs[blockIds[i]].block.getBodyArea();
+
+ if (blockArea.x < rect.left) {
+ rect.left = blockArea.x;
+ }
+ if (blockArea.y < rect.top) {
+ rect.top = blockArea.y;
+ }
+
+ const right = blockArea.x + blockArea.width;
+ const bottom = blockArea.y + blockArea.height;
+ if (right > rect.right) {
+ rect.right = right;
+ }
+ if (bottom > rect.bottom) {
+ rect.bottom = bottom;
+ }
+ }
+
+ const center = {
+ x: (rect.left + rect.right) / 2,
+ y: (rect.top + rect.bottom) / 2,
+ };
+
+ return this.centerOnPoint(center);
+ }
+
+ public showBlockContextMenu(pos: Position2D) {
+ // Base positioning
+ this.popupGroup.innerHTML = '';
+ this.popupGroup.setAttribute('class', 'popup_group context_menu');
+
+ const canvas_rect = this.canvas.getBoundingClientRect();
+ const workspacePos = { x: pos.x - canvas_rect.x, y: pos.y - canvas_rect.y };
+
+ this.popupGroup.style.left = workspacePos.x + 'px';
+ this.popupGroup.style.top = workspacePos.y + 'px';
+ delete this.popupGroup.style.maxHeight;
+
+ // Block operations
+ const block_ops = document.createElement('ul');
+
+ // Default options
+ if (this._selectedBlocks.some(this._canCloneBlock.bind(this))) {
+ const clone_entry = document.createElement('li');
+ clone_entry.innerText = 'Clone';
+ clone_entry.onclick = (ev) => { this.ensureContextMenuHidden(); this.cloneSelection(); };
+
+ block_ops.appendChild(clone_entry);
+ }
+
+ // Single block options
+ if (this._selectedBlocks.length === 1) {
+ const blockId = this._selectedBlocks[0];
+ const block = this.blockObjs[blockId].block;
+ const actions = block.getBlockContextActions();
+
+ for (const action of actions) {
+ const entry = document.createElement('li');
+ entry.innerText = action.title;
+ entry.onclick = (ev) => { this.ensureContextMenuHidden(); action.run(); };
+ block_ops.appendChild(entry);
+ }
+ }
+
+ this.popupGroup.appendChild(block_ops);
+ }
+
+ public cloneSelection(): string[] {
+ const newIds = [];
+
+ // Unselect blocks that cannot be cloned
+ const blocks = this._selectedBlocks.filter(this._canCloneBlock.bind(this));
+
+ for (const blockId of blocks) {
+
+ const blockObj = this.blockObjs[blockId];
+ const data = blockObj.block.serialize();
+
+ const newId = uuidv4();
+ const clone = this.deserializeBlock(newId, data);
+
+ const prevPos = blockObj.block.getBodyArea();
+ this.draw(clone, { x: prevPos.x + 20, y: prevPos.y - 20 });
+ newIds.push(newId);
+ }
+
+ // Replicate connections among the selected blocks
+ for (const connectionId of Array.from(this.connections.keys())) {
+ const connection = this.connections.get(connectionId);
+
+ // Look for matching sink
+ const sink = connection.sink;
+ const sinkIndex = blocks.indexOf(sink.block_id);
+ if (sinkIndex < 0) {
+ continue;
+ }
+
+ // Look for matching source
+ const source = connection.source;
+ const sourceIndex = blocks.indexOf(source.block_id);
+ if (sourceIndex < 0) {
+ continue;
+ }
+
+ this.establishConnection(
+ {
+ block: this.blockObjs[newIds[sourceIndex]].block,
+ type: 'out',
+ index: connection.source.output_index,
+ },
+ {
+ block: this.blockObjs[newIds[sinkIndex]].block,
+ type: 'in',
+ index: connection.sink.input_index,
+ },
+ );
+ }
+
+ this.updateSelectBlockList(newIds);
+
+ return newIds;
+ }
+
+ public ensureContextMenuHidden() {
+ this.popupGroup.classList.remove('context_menu');
+ this.popupGroup.classList.add('hidden');
+ }
+
+ public _getPositionFromEvent(ev: MouseEvent | TouchEvent) : Position2D | null {
+ if ((ev as TouchEvent).targetTouches) {
+ const touchEv = ev as TouchEvent;
+ if (touchEv.targetTouches.length === 0) {
+ return null;
+ }
+ return { x: touchEv.targetTouches[0].clientX, y: touchEv.targetTouches[0].clientY };
+ }
+ else {
+ const mouseEv = ev as MouseEvent;
+ return { x: mouseEv.clientX, y: mouseEv.clientY };
+ }
+ }
+
+ public startResizing(block: Resizeable, ev: MouseEvent | TouchEvent) {
+ const initialPos = this._getPositionFromEvent(ev);
+ const area = block.getBodyArea();
+
+ this.canvas.onmousemove = this.canvas.ontouchmove = ((ev: MouseEvent | TouchEvent) => {
+ const pos = this._getPositionFromEvent(ev);
+
+ const diffX = initialPos.x - pos.x;
+ const diffY = initialPos.y - pos.y;
+
+ const newWidth = area.width - diffX * this.inv_zoom_level;
+ const newHeight = area.height - diffY * this.inv_zoom_level;
+
+ block.resize({ width: newWidth, height: newHeight });
+ });
+
+ this.canvas.onmouseup = this.canvas.ontouchend = ((ev: MouseEvent | TouchEvent) => {
+ this.canvas.onmousemove = null;
+ this.canvas.onmouseup = null;
+ });
+ }
+
+ private _canCloneBlock(blockId: string): boolean {
+ const block = this.blockObjs[blockId].block;
+
+ if (block instanceof ContainerFlowBlock) {
+ if (block.isPage) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private _findContainerInPos(pos: Position2D, excludingList: string[]): FlowBlock | null {
+ const candidates: (ContainerBlock & FlowBlock)[] = [];
+
+ for (const container of this.containers) {
+ if (excludingList.indexOf(container.id) >= 0) {
+ continue;
+ }
+
+ const area = container.getBodyElement().getBoundingClientRect();
+ if (!area) {
+ continue;
+ }
+
+ const diffX = pos.x - area.x;
+ const diffY = pos.y - area.y;
+
+ if ((diffX >= 0) && (diffY >= 0)
+ && (diffX <= area.width)
+ && (diffY <= area.height)) {
+
+ candidates.push(container);
+ }
+ }
+
+ if (candidates.length === 0) {
+ return null;
+ }
+
+ // Candidate priority
+ // 1. First, the ones that are not pages
+ // 2. The ones with smaller area
+ const pages = [];
+ const notPages = [];
+
+ for (const container of candidates) {
+ if (container.isPage) {
+ pages.push(container);
+ }
+ else {
+ notPages.push(container);
+ }
+ }
+
+ const partition = notPages.length > 0 ? notPages : pages;
+ partition.sort((a, b) => {
+ const areaA = a.getBodyArea();
+ const areaB = b.getBodyArea();
+
+ return (areaA.height * areaA.width) - (areaB.height * areaB.width);
+ });
+
+ return partition[0];
+ }
+
+ private _getContainerOfBlock(blockId: string): FlowBlock | null {
+ const blockInfo = this.blocks.get(blockId);
+ if (!blockInfo) {
+ throw new Error("Can't find block info of " + blockId);
+ }
+ if (blockInfo.container_id) {
+
+ if (!blockInfo) {
+ throw new Error("Can't find container: " + blockInfo.container_id);
+ }
+
+ return this.blockObjs[blockInfo.container_id].block;
+ }
+
+ return null;
+ }
+
+ private _updateBlockContainer(block: FlowBlock, container?: FlowBlock) {
+ const block_id = block.id;
+ const wasInContainer = this._getContainerOfBlock(block_id);
+ this._updateBlockContainerFromContainer(block, container, wasInContainer);
+ }
+
+ private _updateBlockContainerFromContainer(block: FlowBlock, container?: FlowBlock, wasInContainer?: FlowBlock) {
+ const block_id = block.id;
+
+ const container_id = container ? container.id : null;
+
+ if (wasInContainer !== container) {
+ if (wasInContainer) {
+ (wasInContainer as any as ContainerBlock).removeContentBlock(block);
+ }
+
+ if (container) {
+ try {
+ (container as any as ContainerBlock).addContentBlock(block);
+ }
+ catch (err) {
+ if (err instanceof CannotSetAsContentsError) {
+ this.blocks.get(block_id).container_id = null;
+ }
+ throw err;
+ }
+ }
+
+ if (this.blocks.has(block_id)) {
+ const data = this.blocks.get(block_id);
+ if (data.container_id !== container_id) {
+ data.container_id = container_id;
+ this.blocks.set(block_id, data);
+ }
+ }
+
+ if (block instanceof UiFlowBlock) {
+ block.updateContainer(container);
+ }
+ }
+ else if (container) {
+ (container as any as ContainerBlock).update();
+ }
+ }
+
+ private _mouseDownOnBlock(pos: Position2D, block: FlowBlock, on_done?: (pos: Position2D) => void) {
+ const block_id = block.id;
+ this.ensureBlockSelected(block_id);
+
+ if (this.state !== 'waiting') {
+ console.error('Forcing start of MouseDown with Workspace state='+this.state);
+ }
+ this.state = 'dragging-block';
+
+ const bodyElement = block.getBodyElement();
+
+ let last = pos;
+ let lastContainer: FlowBlock | null = null;
+
+ this.canvas.onmousemove = this.canvas.ontouchmove = ((ev: MouseEvent | TouchEvent) => {
+ const pos = this._getPositionFromEvent(ev);
+ const container = this._findContainerInPos(pos, this._selectedBlocks.concat([block_id]));
+
+ if (lastContainer !== container) {
+ if (lastContainer) {
+ lastContainer.getBodyElement().classList.remove('highlighted');
+ }
+
+ if (container) {
+ container.getBodyElement().classList.add('highlighted');
+ }
+
+ lastContainer = container;
+ }
+
+
+
+ try {
+ const distance = {
+ x: (pos.x - last.x) * this.inv_zoom_level,
+ y: (pos.y - last.y) * this.inv_zoom_level,
+ };
+ last = {x: pos.x, y: pos.y};
+
+ for (const blockId of this._selectedBlocks) {
+ const container = this._getContainerOfBlock(blockId);
+ const isContainerSelected = container === null ? false : this._selectedBlocks.indexOf(container.id) >= 0;
+ if (isContainerSelected) {
+ // Container of the block is also selected, avoid moving it twice
+ continue;
+ }
+
+ const draggedBlocks = this.blockObjs[blockId].block.moveBy(distance).map(block => block.id);
+ this._afterBlocksMove(draggedBlocks.concat([block.id]));
+ }
+
+ if (this.isInTrashcan(pos)) {
+ this.trashcan.classList.add('to-be-activated');
+ bodyElement.classList.add('to-be-removed');
+ }
+ else {
+ this.trashcan.classList.remove('to-be-activated');
+ bodyElement.classList.remove('to-be-removed');
+ }
+ }
+ catch(err) {
+ console.error(err);
+ }
+ });
+ this.canvas.onmouseup = this.canvas.ontouchend = ((ev: MouseEvent | TouchEvent) => {
+ if (lastContainer) {
+ lastContainer.getBodyElement().classList.remove('highlighted');
+ }
+
+ const oldContainer: string | null = this.blocks.get(block_id).container_id;
+ const pos = this._getPositionFromEvent(ev) || last;
+ const container = this._findContainerInPos(pos, this._selectedBlocks);
+ const containerId = container === null ? null : container.id;
+
+ let moved: string[] = [];
+
+ // Only update container if either:
+ // - The dragged block was in a container and now is not
+ // - The dragged block is dropped in a container not in the selected group
+ if ((oldContainer && (!containerId))
+ || (containerId && (this._selectedBlocks.indexOf(containerId) < 0))) {
+
+ for (const blockId of this._selectedBlocks.concat([])) {
+ const blockInfo = this.blocks.get(blockId);
+ const blockObj = this.blockObjs[blockId];
+
+ try {
+ // Don't update container if it's on the selection
+ if (blockInfo.container_id && this._selectedBlocks.indexOf(blockInfo.container_id) >= 0) {
+ continue;
+ }
+
+ this._updateBlockContainer(blockObj.block, container);
+ }
+ catch (err) {
+ if (err instanceof CannotSetAsContentsError) {
+ console.error("Cannot set as content", err.problematicContents); // TODO: Show as notification
+ this._updateBlockContainer(blockObj.block, null);
+ }
+ }
+ }
+ }
+
+ // Track the blocks dragged
+ for (const blockId of this._selectedBlocks.concat([])) {
+ const draggedBlocks = this.blockObjs[blockId].block.endMove().map(block => block.id);
+
+ moved.push(blockId)
+ moved = moved.concat(draggedBlocks);
+ }
+
+ let removed = false;
+ try {
+ const pos = this._getPositionFromEvent(ev) || last;
+
+ if (on_done) {
+ on_done(pos);
+ }
+
+ this.state = 'waiting';
+ this.canvas.onmousemove = null;
+ this.canvas.onmouseup = null;
+ this.trashcan.classList.remove('to-be-activated');
+ bodyElement.classList.remove('to-be-removed');
+
+ if (this.isInTrashcan(pos)) {
+ removed = true;
+ for (const blockId of moved.concat([])) {
+ this.removeBlock(blockId, this.blocks.get(blockId));
+ }
+ }
+ }
+ catch (err) {
+ console.error(err);
+ }
+
+ // If autoposition is not activated, only move the connections present
+ if (!removed && !this.autoposition) {
+ // Update moved block's connections
+ this._afterBlocksMove(moved);
+
+ // Take into account the old container
+ if (oldContainer) {
+ moved.push(oldContainer);
+ }
+ }
+
+ // Else, just rely on the autopositioning
+ if (this.autoposition) {
+ this.repositionAll();
+ }
+ });
+ }
+
+ private _afterBlocksMove(blockIds: string[]) {
+ for (const movedId of blockIds) {
+ for (const conn of this.blocks.get(movedId).connections) {
+ this.updateConnection(conn);
+ }
+
+ this.updateBlockInputHelpersPosition(movedId);
+ }
+ }
+
+ public invalidateBlock(blockId: string) {
+ this._invalidateBlockPositions([blockId]);
+ }
+
+ private _invalidateBlockPositions(blocks: string[]) {
+ // This would be a good point to save the invalidated blocks and not
+ // launch the repositioning in case the initial "build" is not finished
+ if (this.autoposition) {
+ this._reposition(blocks);
+ }
+ }
+
+ public repositionAll() {
+ const blocks = Object.keys(this.blockObjs);
+ this._reposition(blocks);
+
+ for (const blockId of blocks) {
+ for (const conn of this.blocks.get(blockId).connections) {
+ this.updateConnection(conn);
+ }
+
+ this.updateBlockInputHelpersPosition(blockId);
+ }
+ }
+
+ async repositionIteratively(max_iterations?: number) {
+ // For positioning debugging purposes
+ const its = [];
+
+ if (!max_iterations) {
+ max_iterations = DEFAULT_MAX_ITERATIONS;
+ }
+ if (max_iterations > ABSOLUTE_MAX_ITERATIONS) {
+ max_iterations = ABSOLUTE_MAX_ITERATIONS;
+ console.warn(`Limited max iterations number. ${max_iterations} -> ${ABSOLUTE_MAX_ITERATIONS}`);
+ }
+
+ for (let i = 0; i < max_iterations; i++) {
+ console.time("It " + (i + 1));
+
+ const prevPos: [string, Area2D][] = Object.keys(this.blockObjs).map( id => [id, this.blockObjs[id].block.getBodyArea() ] )
+ this.repositionAll();
+
+ const diffs = prevPos.map(([id, prev]) => {
+ const after = this.blockObjs[id].block.getBodyArea();
+
+ return {
+ block: this.blockObjs[id].block,
+ x: Math.abs(after.x - prev.x),
+ y: Math.abs(after.y - prev.y),
+ width: Math.abs(after.width - prev.width),
+ height: Math.abs(after.height - prev.height),
+ }
+ })
+
+ const mov = {
+ x: maxKey(diffs, e => e.x),
+ y: maxKey(diffs, e => e.y),
+ width: maxKey(diffs, e => e.width),
+ height: maxKey(diffs, e => e.height),
+ };
+ its.push(mov)
+ console.timeEnd("It " + (i + 1));
+ console.debug('Max movement in iteration:',
+ {
+ x: { x: mov.x.x, block: mov.x.block },
+ y: { y: mov.y.y, block: mov.y.block },
+ width: { width: mov.width.width, block: mov.width.block },
+ height: { height: mov.height.x, block: mov.height.block },
+ });
+
+ if ((Math.abs(mov.x.x) < 1) && (Math.abs(mov.y.y) < 1) && (Math.abs(mov.width.width) < 1) && (Math.abs(mov.height.height) < 1)) {
+ console.log("Stable on it", i); // No +1 because it was already stable from last iteration
+ break;
+ }
+
+ await new Promise(resolve => setTimeout(resolve, TIME_BETWEEN_POSITION_ITERATIONS));
+ }
+
+ return its;
+ }
+
+ private _reposition(blockIds: string[]) {
+ this.doc.transact((_transaction: Y.Transaction) => {
+ // Build the list of dependencies (contents) for each block repositioned
+ const dependencies: {[key: string]: string[]} = {};
+
+ const considered: {[key: string]: boolean} = {};
+ for (const id of blockIds) {
+ considered[id] = true;
+ }
+
+ const allAffected = [];
+ const toExplore = blockIds.concat([]);
+ while (toExplore.length > 0) {
+ const id = toExplore.shift();
+ allAffected.push(id);
+
+ const block = this.blocks.get(id);
+
+ if (block.container_id) {
+ const dep = block.container_id;
+ if (!considered[dep]) {
+ toExplore.push(dep);
+ considered[dep] = true;
+ }
+
+ if (!dependencies[dep]){
+ dependencies[dep] = [];
+ }
+
+ dependencies[dep].push(id);
+ }
+ }
+
+ const processed: string[] = [];
+ let toGo = allAffected.concat([]);
+ let processedThisTurn = true;
+ while (toGo.length > 0 && processedThisTurn) {
+ processedThisTurn = false;
+ const skipped = [];
+
+ for (const bId of toGo) {
+ const blockObj = this.blockObjs[bId];
+ if ((dependencies[bId] || []).some(x => processed.indexOf(x) < 0)) {
+ // Not all contents have been repositioned yet
+ skipped.push(bId);
+ }
+ else {
+ const block = blockObj.block;
+ if (block instanceof ContainerFlowBlock) {
+ block.repositionContents();
+ }
+
+ processedThisTurn = true;
+ processed.push(bId);
+ }
+ }
+
+ toGo = skipped;
+ }
+
+ if (toGo.length > 0) {
+ console.error("Circular dependency found on", toGo);
+ console.error("Circular dependency IDS:", toGo.map(id => this.blocks.get(id)));
+ }
+
+ // After all are processed, give then the option to "settle" on their new position
+ for (const elementId of processed.reverse()) {
+ const block = this.blockObjs[elementId].block;
+
+ // This have a reasonably-close semantic, but it might not be
+ // enough. A new function might be needed to cover this meaning.
+ block.endMove();
+ }
+ });
+ }
+
+ private _pullAllDependenciesInList(id: string, group: string[]): string[] {
+ let deps: string[] = [];
+ const block = this.blocks.get(id);
+
+ if (block.container_id && group.indexOf(block.container_id) >= 0) {
+ deps.push(block.container_id);
+
+ const subdeps = this._pullAllDependenciesInList(block.container_id, group);
+
+ if (subdeps.length > 0) {
+ deps = deps.concat(subdeps);
+ }
+ }
+
+ return deps;
+ }
+
+ private isInTrashcan(pos: Position2D): boolean {
+ const rect = this.trashcan.getElementsByTagName('image')[0].getBoundingClientRect();
+ if ((rect.x <= pos.x) && (rect.x + rect.width >= pos.x)) {
+ if ((rect.y <= pos.y) && (rect.y + rect.height >= pos.y)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private drawInputHelper(inputGroup: SVGGElement, inputType: MessageType | 'enum' | 'enum_sequence') {
+ const container = document.createElementNS(SvgNS, 'rect');
+ const connectionLine = document.createElementNS(SvgNS, 'path');
+ const text = document.createElementNS(SvgNS, 'text');
+
+ inputGroup.appendChild(connectionLine);
+ inputGroup.appendChild(container);
+ inputGroup.appendChild(text);
+
+ text.textContent = inputType;
+ text.setAttributeNS(null, 'class', 'text');
+
+ const textBox = text.getBBox();
+ text.setAttributeNS(null, 'transform', `translate(${HELPER_PADDING / 2 -textBox.x - textBox.width / 2}, ${-textBox.y - HELPER_PADDING})`);
+
+ container.setAttributeNS(null, 'class', 'outer_container');
+ container.setAttributeNS(null, 'x', - ((textBox.width) / 2) + '');
+ container.setAttributeNS(null, 'y', - (HELPER_PADDING * 1.5) + '');
+ container.setAttributeNS(null, 'width', HELPER_PADDING + textBox.width + '');
+ container.setAttributeNS(null, 'height', HELPER_PADDING + textBox.height + '');
+ container.setAttributeNS(null, 'rx', '4');
+
+ connectionLine.setAttributeNS(null, 'class', 'connection_line');
+ connectionLine.setAttributeNS(null, 'd',
+ `M${HELPER_PADDING / 2},${- HELPER_PADDING / 2}`
+ + ` L${HELPER_PADDING/2},${HELPER_PADDING / 2 + HELPER_SEPARATION + HELPER_EXTRA_Y}`
+ );
+ }
+
+ private drawBlockInputHelpers(block: FlowBlock, inputHelperGroup?: SVGGElement): SVGGElement {
+ let existing_inputs = 0;
+
+ if (!inputHelperGroup) {
+ inputHelperGroup = document.createElementNS(SvgNS, 'g');
+ }
+ else{
+ existing_inputs = inputHelperGroup.children.length;
+ }
+
+ let inputs = block.getInputs();
+ if (this.read_only) { inputs = []; }
+
+ this.input_helper_section.appendChild(inputHelperGroup);
+
+ let index = -1;
+ for (const input of inputs) {
+ index++;
+
+ if (index < existing_inputs) {
+ continue; // Don't re-draw existing inputs
+ }
+
+ const inputGroup = document.createElementNS(SvgNS, 'g');
+ const input_position = this.getBlockRel(block, block.getPositionOfInput(index));
+
+ let type_class = 'unknown_type';
+ if (input.type) {
+ type_class = input.type + '_port';
+ }
+
+ inputGroup.setAttributeNS(null, 'class', 'input_helper ' + type_class);
+ inputHelperGroup.appendChild(inputGroup);
+
+ this.drawInputHelper(inputGroup, input.type);
+
+ const extra_y = (
+ index % 2 == 0 ? 0
+ : HELPER_EXTRA_Y
+ );
+ inputGroup.setAttributeNS(null, 'transform',
+ `translate(${input_position.x - HELPER_PADDING},`
+ + ` ${input_position.y - HELPER_PADDING / 2 - HELPER_SEPARATION - extra_y})`);
+
+ const element_index = index; // Capture current index
+ inputGroup.onclick = ((_ev: MouseEvent) => {
+ try {
+ const transform = inputGroup.getAttributeNS(null, 'transform');
+
+ const position = { x: 0, y: 0 };
+
+ let translate = transform.match(/translate\(\s*([^\s,]+)\s*,\s*([^\s\)]+)/);
+ if (!translate) {
+ console && console.warn && console.warn('Error getting translation from', inputGroup);
+ }
+ else {
+ position.x = parseInt(translate[1]) + 15;
+ position.y = parseInt(translate[2]) - 15;
+ }
+
+ if (input.type === 'enum') {
+ this.createEnumValue(input, block.id, element_index, { position })
+ }
+ else if (input.type === 'enum_sequence') {
+ this.createEnumValue(input, block.id, element_index, { position })
+ }
+ else {
+ this.createDirectValue(input.type, block.id, element_index, { position });
+ }
+ }
+ catch (err) {
+ console.error("Error creating direct value:", err);
+ }
+ });
+ }
+
+ return inputHelperGroup;
+ }
+
+ private createEnumValue(input: BridgeEnumInputPortDefinition | BridgeEnumSequenceInputPortDefinition,
+ block_id: string, input_index: number,
+ options: { position: Position2D, value?: string }) {
+ const enum_input = new EnumDirectValue({
+ definition: input,
+ get_values: this.getEnum,
+ on_select_requested: this.onSelectRequested.bind(this),
+ on_io_selected: this.onIoSelected.bind(this),
+ }, uuidv4());
+
+ // These two steps use dependent data, so they have to be performed
+ // inside the same transaction.
+ this.doc.transact((_t: Y.Transaction) => {
+ const enum_input_id = this.draw(enum_input, options.position);
+
+ this.addConnection({ block_id: enum_input_id, output_index: 0 },
+ { block_id: block_id, input_index: input_index });
+ });
+ }
+
+ private createDirectValue(type: MessageType, block_id: string, input_index: number,
+ options: { position: Position2D, value?: string }) {
+
+ const direct_input_id = uuidv4();
+ const direct_input = new DirectValue({ type: type,
+ on_request_edit: this.onRequestEdit.bind(this),
+ value: options.value,
+ on_io_selected: this.onIoSelected.bind(this),
+ }, direct_input_id);
+
+ // These two steps use dependent data, so they have to be performed
+ // inside the same transaction.
+ this.doc.transact((_t: Y.Transaction) => {
+ this.draw(direct_input, options.position);
+
+ this.addConnection({ block_id: direct_input_id, output_index: 0 },
+ { block_id: block_id, input_index: input_index });
+ });
+ }
+
+ private static MessageTypeToInputType(type: MessageType): string {
+ if (!type) { type = 'any'; }
+
+ switch(type) {
+ case 'string':
+ case 'any':
+ case 'pulse':
+ case 'user-pulse':
+ return 'text';
+
+ case 'float':
+ case 'integer':
+ return 'number';
+
+ case 'boolean':
+ return 'checkbox';
+ }
+ }
+
+ public editInline(area: Area2D, value: string, type: MessageType, update: (value: string) => void): void {
+ let editor: HTMLInputElement | HTMLTextAreaElement = null;
+ let hiddenEditor = null;
+ if (type === 'boolean') {
+ editor = this.inlineEditor;
+ hiddenEditor = this.inlineMultilineEditor;
+
+ this.inlineEditor.step = '';
+ this.inlineEditor.type = FlowWorkspace.MessageTypeToInputType(type);
+ }
+ else if (type === 'integer') {
+ editor = this.inlineEditor;
+ hiddenEditor = this.inlineMultilineEditor;
+
+ this.inlineEditor.step = '1';
+ this.inlineEditor.type = FlowWorkspace.MessageTypeToInputType(type);
+ }
+ else if (type === 'float') {
+ editor = this.inlineEditor;
+ hiddenEditor = this.inlineMultilineEditor;
+
+ this.inlineEditor.step = '0.1';
+ this.inlineEditor.type = FlowWorkspace.MessageTypeToInputType(type);
+ }
+ else {
+ editor = this.inlineMultilineEditor;
+ hiddenEditor = this.inlineEditor;
+ }
+ editor.value = value;
+
+ if (editor === hiddenEditor) {
+ throw Error("Hidden editor and used one should NOT be the same");
+ }
+
+ const valueArea = this.getWorkspaceRelArea(area);
+
+ this.inlineEditorContainer.style.top = valueArea.y + 2 + 'px';
+ this.inlineEditorContainer.style.left = valueArea.x + 'px';
+ editor.style.width = valueArea.width + 'px';
+ editor.style.height = valueArea.height - 4 + 'px';
+ editor.style.fontSize = (1 / this.inv_zoom_level) * 100 + '%';
+
+ this.inlineEditorContainer.classList.remove('hidden');
+ editor.classList.remove('hidden');
+ hiddenEditor.classList.add('hidden');
+
+ const finishEdition = () => {
+ editor.onblur = null;
+ editor.onkeypress = null;
+ this.inlineEditorContainer.classList.add('hidden');
+
+ if (type === 'boolean') {
+ update(this.inlineEditor.checked ? 'true' : 'false');
+ }
+ else {
+ update(editor.value);
+ }
+ }
+
+ editor.onblur = () => {
+ finishEdition();
+ };
+
+ editor.onkeypress = (ev:KeyboardEvent) => {
+ if ((ev.shiftKey) && (ev.key === 'Enter')) {
+ finishEdition();
+ }
+ };
+
+ editor.focus();
+ }
+
+ private updateBlockInputHelpersPosition(block_id: string) {
+ const blockObj = this.blockObjs[block_id];
+
+ // Deactivate helpers for all inputs in use
+ let index = -1;
+ for (const input of Array.from(blockObj.input_group.children)) {
+ index++;
+
+ const extra_y = (
+ index % 2 == 0 ? 0
+ : HELPER_EXTRA_Y
+ );
+
+ const input_position = this.getBlockRel(blockObj.block, blockObj.block.getPositionOfInput(index));
+ input.setAttributeNS(null, 'transform',
+ `translate(${input_position.x - HELPER_PADDING / 2},`
+ + `${input_position.y - HELPER_PADDING / 2 - HELPER_SEPARATION - extra_y})`);
+ }
+ }
+
+ private updateBlockInputHelpersVisibility(block_id: string) {
+ const blockInfo = this.blocks.get(block_id);
+ const blockObj = this.blockObjs[block_id];
+
+ const inputs_in_use: {[key: string]: boolean} = {};
+ for (const conn_id of blockInfo.connections) {
+ const conn = this.connections.get(conn_id);
+ if (!conn) {
+ continue;
+ }
+
+ if (conn.sink.block_id == block_id) {
+ inputs_in_use[conn.sink.input_index] = true;
+ }
+ }
+
+ // Deactivate helpers for all inputs in use
+ let index = -1;
+ for (const input of Array.from(blockObj.input_group.children)) {
+ index++;
+
+ if (inputs_in_use[index]) {
+ input.classList.add('hidden');
+ }
+ else {
+ input.classList.remove('hidden');
+ }
+ }
+ }
+
+ public get hasPages() {
+ return this.numPages > 0;
+ }
+
+ public removeBlock(blockId: string, info?: SharedBlockData) {
+ if (!info) {
+ info = this.blocks.get(blockId);
+ }
+
+ const blockObj = this.blockObjs[blockId];
+ console.debug("Removing block:", info);
+
+ if (!blockObj) {
+ console.debug("Already removed", blockId);
+
+ return;
+ }
+
+ const slots = blockObj.block.getSlots();
+ if (slots.variable) {
+ if (this.variables_in_use[slots.variable]) {
+ this.variables_in_use[slots.variable]--;
+ }
+ }
+
+
+ if (blockObj.block instanceof ContainerFlowBlock) {
+ const parent_container_id = info.container_id;
+ const parent_container = parent_container_id ? this.blockObjs[parent_container_id].block : null;
+
+ for (const content of blockObj.block.contents.concat([])) {
+ try {
+ this._updateBlockContainer(content, parent_container);
+ }
+ catch (err) {
+ if (err instanceof CannotSetAsContentsError) {
+ this._updateBlockContainer(content, null); // Ignore container
+ }
+ else {
+ throw err;
+ }
+ }
+ }
+
+ if (blockObj.block.isPage) {
+ this.numPages--;
+ }
+ }
+
+ if (this.blocks.has(blockId)) {
+ this._updateBlockContainer(blockObj.block, null);
+ }
+
+ // Make a copy of the array to avoid problems for modifying it during the loop
+ for (const conn_id of info.connections.concat([])) {
+ this.removeConnection(this.connections.get(conn_id));
+ }
+
+ this.input_helper_section.removeChild(blockObj.input_group);
+ blockObj.block.dispose();
+
+ delete this.blockObjs[blockId];
+ if (this.blocks.has(blockId)) {
+ this.blocks.delete(blockId);
+ }
+
+ const idx = this._selectedBlocks.indexOf(blockId);
+ if (idx >= 0) {
+ this._selectedBlocks.splice(idx, 1);
+ }
+ }
+
+ private getBlockRel(block: FlowBlock, position: Position2D): Position2D {
+ const off = block.getOffset();
+ return { x: off.x + position.x, y: off.y + position.y };
+ }
+
+ private getBlockRelArea(block: FlowBlock, area: Area2D): Area2D {
+ const off = block.getOffset();
+
+ return {
+ x: off.x + area.x,
+ y: off.y + area.y,
+ width: area.width,
+ height: area.height,
+ };
+ }
+
+ private absPosToWorkspace(area: Area2D): Area2D {
+ const canvas_rect = this.canvas.getBoundingClientRect();
+ return {
+ x: ((area.x - canvas_rect.left) * this.inv_zoom_level) + this.top_left.x,
+ y: ((area.y - canvas_rect.top) * this.inv_zoom_level) + this.top_left.y,
+ width: area.width * this.inv_zoom_level,
+ height: area.height * this.inv_zoom_level,
+ };
+ }
+
+ private getWorkspaceRelArea(area: Area2D): Area2D {
+ return {
+ x: (area.x - this.top_left.x) / this.inv_zoom_level,
+ y: (area.y - this.top_left.y) / this.inv_zoom_level,
+ width: area.width / this.inv_zoom_level,
+ height: area.height / this.inv_zoom_level,
+ };
+ }
+
+ private current_io_selected: {
+ block: FlowBlock,
+ type: 'in'|'out',
+ index: number,
+ definition: InputPortDefinition | OutputPortDefinition,
+ port_center: Position2D,
+ real_center: Position2D
+ };
+ private current_selecting_connector: SVGElement;
+
+ private drawPath(path: SVGElement,
+ from: Position2D,
+ to: Position2D,
+ runway: number,
+ source_block?: FlowBlock,
+ sink_block?: FlowBlock) {
+ let curve: string;
+
+ let source_runway_direction: Direction2D = 'down';
+ if (source_block) {
+ source_runway_direction = source_block.getOutputRunwayDirection();
+ }
+
+ let bezier_curve = (from.y < to.y);
+ if (source_block && (DirectValue === (source_block as any).__proto__.constructor)) {
+ // Never use bezier curve if the target is DirectInput
+ bezier_curve = true;
+ }
+ else if (!bezier_curve && sink_block) {
+ // Another option: if a sink block was passed and the `from` point
+ // Y position is within the top and bottom of the sink, use bezier even if the position does not match.
+ const area = sink_block.getBodyArea();
+ bezier_curve = (from.y < (area.y + area.height / 2));
+ }
+
+
+ if (bezier_curve) { // Just draw a bezier curve
+ const bezier_runway = runway * 2; // Compensate smoothing of the runway
+
+ const from_runway = FlowWorkspace.addRunway(from, source_runway_direction, bezier_runway);
+ const to_runway = FlowWorkspace.addRunway(to, 'up', bezier_runway);
+
+ curve = [
+ "M", from.x, ",", from.y,
+ " C", from_runway.x, ",", from_runway.y,
+ " ", to_runway.x, ",", to_runway.y,
+ " ", to.x, ",", to.y,
+ ].join("");
+ }
+ else { // Draw a linear circuit
+
+ // We just try to find the X point (where the line goes "up").
+ // We don't try to find the Y point and instead just use fixed "runways".
+ // This makes finding the route simpler and is good enough for now.
+
+ const from_runway = FlowWorkspace.addRunway(from, source_runway_direction, runway);
+ const to_runway = FlowWorkspace.addRunway(to, 'up', runway);
+
+ const x_cut_point = this.find_x_cut_point(from_runway, to_runway);
+
+ curve = [
+ "M", from.x, ",", from.y,
+ " L", from_runway.x, ",", from_runway.y,
+ " L", x_cut_point, ",", from_runway.y,
+ " L", x_cut_point, ",", to_runway.y,
+ " L", to_runway.x, ",", to_runway.y,
+ " L", to.x, ",", to.y,
+ ].join("");
+ }
+
+ path.setAttributeNS(null, "d", curve);
+ path.setAttributeNS(null, 'fill', 'none');
+ path.setAttributeNS(null, 'stroke', 'black');
+ path.setAttributeNS(null, 'stroke-width', '1');
+ }
+
+ private static addRunway(p: Position2D, direction: Direction2D, runway: number) {
+ switch (direction) {
+ case 'up':
+ return { x: p.x, y: p.y - runway };
+ case 'down':
+ return { x: p.x, y: p.y + runway };
+ case 'left':
+ return { x: p.x - runway, y: p.y };
+ case 'right':
+ return { x: p.x + runway, y: p.y };
+ }
+ }
+
+ private find_x_cut_point(from: Position2D, to: Position2D): number {
+ const occupied_sections: { left: number, right: number }[] = [];
+
+ let top = from, bottom = to;
+ if (from.y > to.y) {
+ top = to;
+ bottom = from;
+ }
+
+ for (const block_id of Object.keys(this.blockObjs)) {
+ const block = this.blockObjs[block_id].block;
+ if (block instanceof ContainerFlowBlock) {
+ continue;
+ }
+
+ const body = block.getBodyArea();
+ if (((body.y + body.height) > top.y) && ((body.y < bottom.y))) {
+ occupied_sections.push( { left: body.x, right: body.x + body.width } );
+ }
+ }
+
+ let cut_point = Math.min(from.x, to.x) + CUT_POINT_SEARCH_INCREASES;
+
+ while (true) {
+ let increase = CUT_POINT_SEARCH_INCREASES;
+
+ // Valid cut point?
+ let safe_point = true;
+ for (const section of occupied_sections) {
+ // X-axis position (with any Y-value) falls inside the block?
+ if ((cut_point > section.left) && (cut_point < section.right)) {
+ increase = section.right - cut_point + CUT_POINT_SEARCH_SPACING;
+ safe_point = false;
+ break;
+ }
+ }
+
+ if (safe_point) {
+ return cut_point;
+ }
+
+ cut_point += increase;
+ }
+ }
+
+ isCompatibleConnection(output: string, input: string) : boolean {
+ // If type matches, nothing more to check
+ if (output === input) {
+ return true;
+ }
+
+ // If one of the two are undefined log a warning and allow it
+ if ((!output) || (!input)) {
+ console.error(`Cannot check compatible connection when input type (${input}) or output type (${output}) are inexistent. Defaulting to true to avoid crashes for now.`) ;
+
+ return true;
+ }
+
+ // Special cases
+ if (input === 'string') {
+ // Strings might also come from numbers or bools
+ return [
+ 'any',
+
+ 'string',
+ 'integer',
+ 'float',
+ 'boolean',
+ ].indexOf(output) >= 0;
+ }
+ else if (input === 'integer') {
+ // Integers might also come from floats or bools
+ return [
+ 'any',
+
+ 'integer',
+ 'float',
+ 'boolean',
+ ].indexOf(output) >= 0;
+ }
+ else if (input === 'float') {
+ // Floats might also come from ints or bools
+ return [
+ 'any',
+
+ 'float',
+ 'integer',
+ 'boolean',
+ ].indexOf(output) >= 0;
+ }
+ else if (input === 'any') {
+ // Any accepts anything but pulse
+ return !([
+ 'pulse',
+ 'user-pulse',
+ ].indexOf(output) >= 0);
+ }
+ else if ((input === 'pulse') || (input === 'user-pulse')) {
+ // Pulses just accept pulses
+ return [
+ 'pulse',
+ 'user-pulse',
+ ].indexOf(output) >= 0;
+ }
+ // Right now the outputs are still not recognized as `enum_sequence`
+ // TODO: Remove this when `enum_sequence` outputs are correctly recognized.
+ else if (input === 'enum_sequence') {
+ return [
+ 'enum_sequence',
+
+ 'enum',
+ ].indexOf(output) >= 0;
+ }
+ else if (input === 'list') {
+ // List inputs can only receive lists or any
+ return [
+ 'any',
+ 'list',
+ ].indexOf(output) >= 0;
+ }
+ }
+
+ addConnection(from_: SourceDefinition,
+ to: SinkDefinition,
+ ): boolean {
+
+ if (!(from_.block_id in this.blockObjs) || !(to.block_id in this.blockObjs)) {
+ console.error("Trying to create connection from non-spawned blocks",
+ {
+ from: from_,
+ to: to,
+ from_exists: (from_.block_id in this.blockObjs),
+ to_exists: to.block_id in this.blockObjs
+ });
+
+ return;
+ }
+
+ const sourceObj = this.blockObjs[from_.block_id];
+
+ const source = this.blocks.get(from_.block_id);
+ const source_output_type = sourceObj.block.getOutputType(from_.output_index);
+
+ const sinkObj = this.blockObjs[to.block_id];
+ const sink_input_type = sinkObj.block.getInputType(to.input_index);
+
+ if (!this.isCompatibleConnection(source_output_type, sink_input_type)) {
+ throw new IncompatibleConnectionError(`Can't connect '${source_output_type}' to '${sink_input_type}'`);
+ }
+
+ // The combination (output block&port) -> (input block&port) should be unique.
+ const id = `${from_.block_id}:${from_.output_index}--${to.block_id}:${to.input_index}`;
+
+ if (id in this.connectionElements) {
+ console.debug("Connection already exists");
+ return;
+ }
+
+ const path = document.createElementNS(SvgNS, 'path');
+
+ const conn : FlowConnectionData = { id: id, source: from_, sink: to, type: source_output_type };
+
+ if ((source_output_type == 'pulse') || (source_output_type == 'user-pulse')) {
+ path.setAttributeNS(null, 'marker-end', 'url(#pulse_head)');
+ path.onmouseenter = () => {
+ path.setAttributeNS(null, 'marker-end', 'url(#pulse_head_selected)');
+ };
+ path.onmouseleave = () => {
+ path.setAttributeNS(null, 'marker-end', 'url(#pulse_head)');
+ };
+ }
+
+ setConnectionType(source_output_type, conn, path);
+ path.onclick = () => {
+ if (this.read_only) { return }
+
+ this.removeConnection(conn);
+ };
+ this.connection_group.appendChild(path);
+
+ const sink = this.blocks.get(conn.sink.block_id);
+
+ sourceObj.block.addConnection('out', conn.source.output_index, sinkObj.block, source_output_type);
+ source.connections.push(conn.id);
+
+ sink.connections.push(conn.id);
+ const hasChanged = sinkObj.block.addConnection('in', conn.sink.input_index, sourceObj.block, source_output_type);
+
+ this.connectionElements[conn.id] = path;
+
+ if (!this.connections.has(conn.id)) {
+ this.connections.set(conn.id, conn);
+ }
+ this.updateBlockInputHelpersVisibility(conn.sink.block_id);
+
+ if (hasChanged) {
+ this.propagateChangesFrom(conn.sink.block_id);
+ }
+
+ this.updateConnection(conn.id);
+
+ return true;
+ }
+
+ private removeConnection(conn: FlowConnectionData) {
+ const sourceObj = this.blockObjs[conn.source.block_id];
+ const sinkObj = this.blockObjs[conn.sink.block_id];
+
+ const source = this.blocks.get(conn.source.block_id);
+ const sink = this.blocks.get(conn.sink.block_id);
+
+ // Disconnect from source
+ sourceObj?.block.removeConnection('out', conn.source.output_index, sinkObj.block);
+ if (source) {
+ const source_conn_index = source.connections.indexOf(conn.id);
+ if (source_conn_index < 0) {
+ console.error('Connection not found when going to remove. For block', source);
+ }
+ else {
+ source.connections.splice(source_conn_index, 1);
+ }
+
+ this.updateBlockInputHelpersVisibility(conn.source.block_id);
+ }
+
+ // Disconnect from sink
+ const hasChanged = sinkObj?.block.removeConnection('in', conn.sink.input_index, sourceObj.block);
+ if (sink) {
+ const sink_conn_index = sink.connections.indexOf(conn.id);
+ if (sink_conn_index < 0) {
+ console.error('Connection not found when going to remove. For block', sink);
+ }
+ else {
+ sink.connections.splice(sink_conn_index, 1);
+ }
+ this.updateBlockInputHelpersVisibility(conn.sink.block_id);
+ }
+
+ // Remove workspace information
+ this.connection_group.removeChild(this.connectionElements[conn.id]);
+
+ delete this.connectionElements[conn.id];
+ if (this.connections.has(conn.id)) {
+ this.connections.delete(conn.id);
+ }
+
+ if (hasChanged) {
+ this.propagateChangesFrom(conn.sink.block_id);
+ }
+ }
+
+ private propagateChangesFrom(originBlockId: string) {
+ const considered: {[key: string]: boolean} = {};
+ considered[originBlockId] = true;
+
+ const todo = [originBlockId];
+
+ while (todo.length > 0) {
+ const next = todo.pop();
+ const info = this.blocks.get(next);
+ const blockObj = this.blockObjs[next];
+
+ const linksFrom: [FlowConnectionData, SVGElement][] = [];
+ const linksTo: [FlowConnectionData, SVGElement][] = [];
+
+ // Explore where does this block lead to
+ for (const connId of info.connections) {
+ const connection = this.connections.get(connId);
+ if (connection.source.block_id === next) {
+ const sink = connection.sink.block_id;
+ linksFrom.push([this.connections.get(connId), this.connectionElements[connId]]);
+
+ if (!considered[sink]) {
+ considered[sink] = true;
+ todo.push(sink);
+ }
+ }
+ else {
+ linksTo.push([this.connections.get(connId), this.connectionElements[connId]]);
+ }
+ }
+
+ // Consider changes needed
+ // *Right now* only AtomicFlowBlocks need this
+ // TODO: Extend this to all blocks when type propagation is applied to more block types
+ if (blockObj.block instanceof AtomicFlowBlock) {
+ blockObj.block.refreshConnectionTypes(linksFrom, linksTo);
+ }
+ }
+ }
+
+ updateConnection(connection_id: string) {
+ const conn = this.connections.get(connection_id);
+
+ if (!conn) {
+ console.debug("Trying to update connection before it is available");
+ return;
+ }
+
+ const runway = 20;
+
+ // Source
+ const source = conn.source;
+ if (!(source) || !(source.block_id in this.blockObjs)) {
+ console.warn("Trying to update connection before SOURCE is available");
+ return;
+ }
+
+ const source_block = this.blockObjs[source.block_id].block;
+
+ const source_position = this.getBlockRel(source_block, source_block.getPositionOfOutput(source.output_index));
+
+ // Sink
+ const sink = conn.sink;
+
+ if (!(sink) || !(sink.block_id in this.blockObjs)) {
+ console.warn("Trying to update connection before SINK is available");
+ return;
+ }
+
+ const sink_block = this.blockObjs[sink.block_id].block;
+
+ const element = this.connectionElements[connection_id];
+ if (!element) {
+ console.warn("Trying to update connection before it is rendered");
+ return;
+ }
+
+ const connector_with_marker = !!element.getAttributeNS(null, 'marker-end');
+ const y_sink_offset = connector_with_marker ? 2 : 0;
+
+ const sink_position = this.getBlockRel(sink_block, sink_block.getPositionOfInput(sink.input_index, connector_with_marker));
+ sink_position.y -= y_sink_offset;
+
+ // Draw
+ this.drawPath(element, source_position, sink_position, runway, source_block, sink_block);
+ }
+
+ establishConnection(node1: ConnectableNode, node2: ConnectableNode): boolean {
+ if ((node1.type === node2.type)) { // An input and an output is required
+ return false;
+ }
+
+ if (node1.block === node2.block) {
+ // Let's not do this intentionally, as removing them might be difficult
+ // if this is needed, use an intermediate block.
+ return false;
+ }
+
+ let source = node2;
+ let sink = node1;
+ if (node1.type === 'out') {
+ source = node1;
+ sink = node2;
+ }
+
+ return this.addConnection({block_id: source.block.id, output_index: source.index },
+ {block_id: sink.block.id, input_index: sink.index });
+ }
+
+ private disconnectIOSelected() {
+ this.canvas.classList.remove('drawing');
+ this.canvas.removeChild(this.current_selecting_connector);
+ this.current_selecting_connector = null;
+ this.current_io_selected = null;
+
+ this.canvas.onmousemove = null;
+ this.canvas.onclick = null;
+ }
+
+ // Block manager interface
+ onInputsChanged(block: FlowBlock,
+ _input_num: number,
+ ): void {
+
+ const block_id = block.id;
+ this.drawBlockInputHelpers(block, this.blockObjs[block_id].input_group);
+ }
+
+ onIoSelected(block: FlowBlock,
+ type: 'in'|'out',
+ index: number,
+ definition: InputPortDefinition | OutputPortDefinition,
+ port_center: Position2D,
+ ): void {
+ if (this.read_only) { return; }
+
+ if (!this.current_io_selected) {
+ const real_center = this.getBlockRel(block, port_center);
+ this.current_io_selected = { block, type, index, definition, port_center, real_center };
+
+
+ let type_class = "unknown_wire";
+ if (definition.type) {
+ type_class = definition.type + '_wire';
+ }
+
+ this.current_selecting_connector = document.createElementNS(SvgNS, 'path');
+ this.current_selecting_connector.setAttributeNS(null, 'class', 'building connection ' + type_class);
+ this.canvas.appendChild(this.current_selecting_connector);
+ this.canvas.classList.add('drawing');
+
+ const runway = 20;
+
+ this.canvas.onmousemove = ((ev: any) => {
+ if (!this.canvas.contains(ev.target)) {
+ return;
+ }
+
+ const real_pointer = {
+ x: (ev.x - this.canvas.getBoundingClientRect().left) * this.inv_zoom_level + this.top_left.x,
+ y: (ev.y - this.canvas.getBoundingClientRect().top) * this.inv_zoom_level + this.top_left.y,
+ };
+
+ if (type == 'out') {
+ this.drawPath(this.current_selecting_connector,
+ real_center,
+ real_pointer,
+ runway,
+ block);
+ }
+ else {
+ this.drawPath(this.current_selecting_connector,
+ real_pointer,
+ real_center,
+ runway,
+ null,
+ block);
+ }
+ });
+
+ this.canvas.onclick = ((ev: any) => {
+ if (ev.target === this.canvas) {
+ this.disconnectIOSelected();
+ }
+ });
+ }
+ else {
+ try {
+ if (this.establishConnection(this.current_io_selected,
+ { block, type, index })){
+ this.disconnectIOSelected();
+ }
+ }
+ catch (error) {
+ console.error(error);
+ if (error instanceof IncompatibleConnectionError) {
+ this.toastr.error(error.message, 'Incompatible connection', {
+ closeButton: true,
+ progressBar: true,
+ });
+ this.disconnectIOSelected();
+ }
+ }
+ }
+ }
+
+
+ onSelectRequested(block: FlowBlock,
+ previous_value: string,
+ values: EnumValue[],
+ value_dict: {[key:string]: EnumValue},
+ update: (new_value: string) => void) : void {
+ if (this.read_only) { return; }
+
+ const backdrop = document.createElement('div');
+ this.baseElement.appendChild(backdrop);
+ backdrop.setAttribute('class', 'backdrop');
+
+ const global_pos = block.getBodyElement().getBoundingClientRect();
+ const canvas_rect = this.canvas.getBoundingClientRect();
+ const abs_pos = { x: global_pos.x - canvas_rect.x, y: global_pos.y - canvas_rect.y };
+
+ // TODO: Make this popup separate from the original block.
+ // That should allow avoiding to hand-calculate coordinates, making it more responsive
+
+ // Compensate dropdown stroke-width
+ abs_pos.x -= 1;
+ abs_pos.y -= 1;
+
+ // Base positioning
+ this.popupGroup.innerHTML = '';
+ this.popupGroup.classList.remove('hidden');
+
+ this.popupGroup.style.left = abs_pos.x + 'px';
+ this.popupGroup.style.top = abs_pos.y + 'px';
+ this.popupGroup.style.maxHeight = canvas_rect.height - abs_pos.y + 'px';
+
+ // Editor
+ const editor_container = document.createElement('div');
+ const editor_input = document.createElement('input');
+
+ editor_input.value = '';
+
+ editor_input.style.minHeight = '3em';
+ editor_container.setAttribute('class', 'editor');
+ this.popupGroup.appendChild(editor_container);
+ editor_container.appendChild(editor_input);
+
+ // Option list (now empty)
+ const options = document.createElement('ul');
+ options.setAttribute('class', 'options');
+ options.style.maxHeight = canvas_rect.height - editor_input.getBoundingClientRect().height - abs_pos.y - 1 + 'px';
+
+ this.popupGroup.appendChild(options);
+
+ // Set callbacks functions
+ const close = () => {
+ this.baseElement.removeChild(backdrop);
+ this.popupGroup.innerHTML = '';
+ this.popupGroup.classList.add('hidden');
+ };
+ const select_value = (val: string) => {
+ close();
+ this._notifyChangedVariable(previous_value, val);
+
+ update(val);
+ };
+
+
+ const MAX_RESULTS = 100; // Max. results to show at a single time
+ const TIME_WAIT_FOR_SEARCH_TIME = 200; // Time to wait for next input before attempting to search
+ const SCROLL_OPTIONS: ScrollIntoViewOptions = {
+ behavior: 'smooth',
+ block: 'nearest',
+ };
+
+ let bounce_control: NodeJS.Timeout = null;
+ let last_query: string = null;
+
+ let selected_index: number = null;
+
+ // Keyup if controled instead of keypress
+ // as ArrowRight and ArrowLeft are not triggered on keypress
+ editor_input.onkeyup = (ev: KeyboardEvent) => {
+ if (ev.key === 'ArrowUp' || ev.key === 'ArrowDown'
+ || ev.key === 'PageDown' || ev.key === 'PageUp'
+ || ev.key === 'Home' || ev.key === 'End'
+ ) {
+ const old_index = selected_index;
+ let scroll_options: ScrollIntoViewOptions = { behavior: SCROLL_OPTIONS.behavior, block: SCROLL_OPTIONS.block };
+
+ if (ev.key === 'ArrowUp') {
+ if (selected_index) {
+ selected_index--;
+ }
+ else {
+ selected_index = 0;
+ }
+ }
+ else if (ev.key === 'ArrowDown') {
+ if (selected_index === null) {
+ selected_index = 0;
+ }
+ else {
+ selected_index++;
+ }
+ }
+ else if (ev.key === 'PageDown') {
+ if (selected_index === null) {
+ selected_index = 0;
+ }
+ else {
+ scroll_options.block = 'start';
+ const children = options.children;
+ const parent = options.getBoundingClientRect();
+ // Go down the list until an element is not in view
+ // this is not done directly with `inc = size(container) / size(next_element)`
+ // to support for cases where elements have different sizes
+ for (; selected_index < children.length; selected_index++) {
+ const child = children[selected_index].getBoundingClientRect();
+ if (child.bottom > parent.bottom) {
+ break;
+ }
+ }
+ }
+ }
+ else if (ev.key === 'PageUp') {
+ if (!selected_index) {
+ selected_index = 0;
+ }
+ else {
+ selected_index--;
+ scroll_options.block = 'end';
+ const children = options.children;
+ const parent = options.getBoundingClientRect();
+ // Go down the list until an element is not in view
+ // this is not done directly with `inc = size(container) / size(next_element)`
+ // to support for cases where elements have different sizes
+ for (; selected_index > 0; selected_index--) {
+ const child = children[selected_index].getBoundingClientRect();
+ if (child.top < parent.top) {
+ break;
+ }
+ }
+ }
+ }
+ else if (ev.key === 'Home') {
+ selected_index = 0;
+ }
+ else if (ev.key === 'End') {
+ selected_index = options.children.length - 1;
+ }
+
+ try {
+ if (old_index !== null) {
+ options.children[old_index].classList.remove('selected');
+ }
+
+ const selected = options.children[selected_index];
+ selected.classList.add('selected');
+ selected.scrollIntoView(scroll_options);
+ }
+ catch(err) {
+ console.warn(err);
+ }
+ }
+ else if (ev.key === 'Enter') {
+ if (selected_index !== null) {
+ (options.children[selected_index] as HTMLElement).click();
+ }
+ }
+
+ // Avoid updating when no change has been made
+ if (last_query !== editor_input.value) {
+ if (bounce_control) {
+ clearTimeout(bounce_control);
+ }
+ bounce_control = setTimeout(() => {
+ bounce_control = null;
+ update_values();
+ }, TIME_WAIT_FOR_SEARCH_TIME);
+ }
+ }
+
+ backdrop.onclick = () => {
+ close();
+ };
+
+ const update_values = () => {
+ selected_index = null;
+ const query = editor_input.value;
+ last_query = query;
+
+ let matches = values;
+ if (query) {
+ const options = {
+ isCaseSensitive: false,
+ findAllMatches: false,
+ includeMatches: false,
+ includeScore: true,
+ useExtendedSearch: false,
+ minMatchCharLength: 1,
+ shouldSort: true,
+ threshold: 0.6,
+ location: 0,
+ distance: 10,
+ keys: [
+ "name",
+ ]
+ };
+
+ const fuse = new Fuse(values, options);
+
+ // Change the pattern
+ const results = fuse.search(query);
+ results.sort((x: any, y: any) => (x as any).score - (y as any).score );
+ if (!matches) {
+ return; // Don't update
+ }
+
+ matches = results.map((v: { item: any; }) => v.item);
+ }
+
+ // Options
+ options.innerHTML = ''; // Clear children
+ for (const value of matches.slice(0, MAX_RESULTS)) {
+ const e = document.createElement('li');
+ e.innerText = value.name;
+ options.appendChild(e);
+ e.onclick = () => {
+ select_value(value.id);
+ }
+ }
+ // Scroll options up
+ options.children[0].scrollIntoView(SCROLL_OPTIONS);
+ };
+
+ update_values();
+ editor_input.focus();
+ }
+
+ onDropdownExtended(block: FlowBlock,
+ slot_id: string,
+ previous_value: string,
+ current_rect: Area2D,
+ update: (new_value: string) => void,
+ ): void {
+ if (this.read_only) { return; }
+
+ const backdrop = document.createElement('div');
+ this.baseElement.appendChild(backdrop);
+ backdrop.setAttribute('class', 'backdrop');
+
+ const edition_area = this.getBlockRelArea(block, current_rect);
+ const abs_area = this.getWorkspaceRelArea(edition_area);
+
+ // Compensate dropdown stroke-width
+ abs_area.x -= 1;
+ abs_area.y -= 1;
+ abs_area.width += 2;
+ abs_area.height += 2;
+
+ // Base positioning
+ this.popupGroup.innerHTML = '';
+
+ this.popupGroup.style.left = abs_area.x + 'px';
+ this.popupGroup.style.top = abs_area.y + 'px';
+
+ // Editor
+ const editor_container = document.createElement('div');
+ const editor_input = document.createElement('input');
+
+ editor_input.value = previous_value;
+ editor_input.style.minWidth = abs_area.width + 'px';
+ editor_input.style.minHeight = abs_area.height + 'px';
+ editor_container.setAttribute('class', 'editor');
+ this.popupGroup.appendChild(editor_container);
+ editor_container.appendChild(editor_input);
+
+ // Set callbacks functions
+ const close = () => {
+ this.baseElement.removeChild(backdrop);
+ this.popupGroup.innerHTML = '';
+ this.popupGroup.classList.add('hidden');
+ };
+ const select_value = (val: string) => {
+ close();
+ this._notifyChangedVariable(previous_value, val);
+
+ update(val);
+ };
+
+ editor_input.onkeypress = (ev:KeyboardEvent) => {
+ if (ev.key === 'Enter') {
+ select_value(editor_input.value);
+ }
+ };
+
+ backdrop.onclick = () => {
+ close();
+ };
+
+ // Options
+ const options = document.createElement('ul');
+ options.setAttribute('class', 'options');
+ this.popupGroup.appendChild(options);
+
+ let option_list = [];
+ if (slot_id === 'variable') {
+ for (const var_name of Object.keys(this.variables_in_use)) {
+ if (this.variables_in_use[var_name] > 0) {
+ option_list.push(var_name);
+ }
+ }
+ }
+
+ if (option_list.length === 0) {
+ option_list.push('i');
+ }
+
+ for (const option of option_list) {
+ const e = document.createElement('li');
+ e.innerText = option;
+ options.appendChild(e);
+ e.onclick = () => {
+ select_value(option);
+ }
+ }
+
+ this.popupGroup.classList.remove('hidden');
+ }
+
+ _notifyChangedVariable(prevValue: string, newValue: string) {
+ if (this.variables_in_use[prevValue]) {
+ this.variables_in_use[prevValue]--;
+ }
+
+ if (!this.variables_in_use[newValue]) {
+ this.variables_in_use[newValue] = 0;
+ }
+ this.variables_in_use[newValue]++;
+ }
+
+ onRequestEdit(block: DirectValue, type: MessageType, update: (value: string) => void): void {
+ this.editInline(block.getValueArea(), block.value, type, update);
+ }
+ //
+
+ // Block configuration
+ startBlockConfiguration(block: ConfigurableBlock) {
+ const dialogRef = this.dialog.open(ConfigureBlockDialogComponent, {
+ data: { block: block, programId: this.programId }
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!(result && result.success)) {
+ console.log("Cancelled");
+ return;
+ }
+
+ block.applyConfiguration((result.settings as BlockConfigurationOptions));
+ });
+ }
+
+ getAssetUrlOnProgram(assetId: string): string {
+ return this.programService.getAssetUrlOnProgram(assetId, this.programId);
+ }
+}
diff --git a/frontend/src/app/flow-editor/graph_analysis.ts b/frontend/src/app/flow-editor/graph_analysis.ts
new file mode 100644
index 00000000..16af801e
--- /dev/null
+++ b/frontend/src/app/flow-editor/graph_analysis.ts
@@ -0,0 +1,2078 @@
+import { AtomicFlowBlock, AtomicFlowBlockData, AtomicFlowBlockOptions, isAtomicFlowBlockOptions, isAtomicFlowBlockData } from './atomic_flow_block';
+import { BaseToolboxDescription, ToolboxDescription } from './base_toolbox_description';
+import { DirectValueFlowBlockData, isDirectValueBlockData } from './direct_value';
+import { EnumDirectValueFlowBlockData, isEnumDirectValueBlockData, EnumDirectValue } from './enum_direct_value';
+import { CompiledBlock, CompiledBlockArg, CompiledBlockArgs, CompiledFlowGraph, ContentBlock, FlowGraph, FlowGraphEdge, FlowGraphNode, CompiledBlockArgList, CompiledBlockType, CompiledBlockArgMonitorDict, CompiledBlockArgCallServiceDict, CompiledBlockServiceCallSelectorArgs } from './flow_graph';
+import { extract_internally_reused_arguments, is_pulse_output, lift_common_ops, scan_downstream, scan_upstream, split_streaming_after_stepped, is_pulse } from './graph_transformations';
+import { index_connections, reverse_index_connections, EdgeIndex, IndexedFlowGraphEdge } from './graph_utils';
+import { TIME_MONITOR_ID } from './platform_facilities';
+import { uuidv4 } from './utils';
+import { isUiFlowBlockData } from './ui-blocks/ui_flow_block';
+
+function index_toolbox_description(desc: ToolboxDescription): {[key: string]: AtomicFlowBlockOptions} {
+ const result: {[key: string]: AtomicFlowBlockOptions} = {};
+
+ for (const cat of desc) {
+ for (const block of cat.blocks) {
+ // TODO: This will most probably require UI block definitions too
+ if (isAtomicFlowBlockOptions(block)) {
+ result[block.block_function] = block;
+ }
+ }
+ }
+
+ return result;
+}
+
+const BASE_TOOLBOX_BLOCKS = index_toolbox_description(BaseToolboxDescription);
+
+const JUMP_TO_POSITION_OPERATION = 'jump_to_position';
+const JUMP_TO_BLOCK_OPERATION = 'jump_to_block';
+const FORK_OPERATION = 'op_fork_execution';
+const REPEAT_OPERATION = 'control_repeat';
+const TIME_TRIGGERS = ['flow_utc_date', 'flow_utc_time'];
+
+function makes_reachable(conn: FlowGraphEdge, block: FlowGraphNode): boolean {
+ if (isAtomicFlowBlockData(block.data)){
+ const data = block.data;
+
+ if (data.value.options.type !== 'operation') {
+ // Getter or trigger
+ return true;
+ }
+
+ const input = data.value.options.inputs[conn.to.input_index];
+ if (!input) {
+ const extras = data.value.options.extra_inputs;
+ if (extras) {
+ return is_pulse(extras);
+ }
+
+ throw new Error(`No input #${conn.to.input_index} on ${JSON.stringify(data.value.options.inputs)} [conn: ${JSON.stringify(conn)}]`);
+ }
+
+ return is_pulse(input);
+ }
+ else if (isDirectValueBlockData(block.data)){
+ throw new Error('Connection from reached block to value (backwards?)');
+ }
+ else if (isEnumDirectValueBlockData(block.data)){
+ throw new Error('Connection from reached block to value (backwards?)');
+ }
+ else if (isUiFlowBlockData(block.data)) {
+ return true;
+ }
+}
+
+function build_index(arr: string[]): { [key:string]: boolean} {
+ const index: {[key: string]: boolean} = {};
+
+ for (const element of arr) {
+ index[element] = true;
+ }
+
+ return index;
+}
+
+function set_difference(whole: any[], subset: {[key: string]: boolean}|string[]): any[] {
+ const result = [];
+
+ if (subset.map) { // Is an array
+ subset = build_index(subset as string[]);
+ }
+
+ const subsetMap = subset as {[key: string]: boolean};
+
+ for (const element of whole) {
+ if (!subsetMap[element]) {
+ result.push(element);
+ }
+ }
+
+ return result;
+}
+
+function is_getter_node(block: FlowGraphNode): boolean {
+ if (isAtomicFlowBlockData(block.data)){
+ const data = block.data as AtomicFlowBlockData;
+
+ if (data.value.options.type !== 'operation') {
+ // Getter or trigger
+ return true;
+ }
+ return false;
+ }
+ else if (isDirectValueBlockData(block.data)){
+ return true;
+ }
+ else if (isEnumDirectValueBlockData(block.data)){
+ return true;
+ }
+}
+
+
+function is_trigger_node(graph: FlowGraph, block_id: string, conn_index: EdgeIndex, rev_conn_index: EdgeIndex): boolean {
+ const block = graph.nodes[block_id];
+ if (isAtomicFlowBlockData(block.data) || isUiFlowBlockData(block.data)){
+ const data = block.data;
+
+ const inputs = data.value.options.inputs || [];
+
+ // If it has any pulse input, it's not a source block
+ if (inputs.filter(v => is_pulse(v)).length > 0) {
+ return false;
+ }
+
+ // If it has a getter input, it's not a source block (the getter is)
+ let has_block_inputs = false;
+ for (const conn of rev_conn_index[block_id] || []) {
+ const orig = graph.nodes[conn.from.id];
+ if (orig.data.type === AtomicFlowBlock.GetBlockType()) {
+ has_block_inputs = true;
+ break;
+ }
+ }
+
+ if (has_block_inputs) {
+ return false;
+ }
+
+ // If it's a getter check that it gets derived into a trigger
+ if (data.value.options.type === 'getter') {
+ const find_triggers_downstream_controller = ((_node_id: string, node: FlowGraphNode, _: string[]) => {
+ if (isAtomicFlowBlockData(node.data)) {
+ const a_node = node.data;
+
+ if (a_node.value.options.type === 'getter') {
+ return 'continue'; // This path might be valid, continue checking downstream
+ }
+ else if (a_node.value.options.type === 'operation') {
+ return 'stop'; // This path is not valid
+ }
+ else if (a_node.value.options.type === 'trigger') {
+ return 'capture'; // This is a valid path
+ }
+ }
+ else if (isUiFlowBlockData(node.data)) {
+ const hasSignalInput = !!((node.data.value.options.inputs || []).find(inp => is_pulse(inp)));
+ const hasSignalOutput = !!((node.data.value.options.outputs || []).find(outp => is_pulse(outp)));
+
+ if (!hasSignalInput && hasSignalOutput) {
+ // Trigger block, like a button
+ return 'capture'
+ }
+
+ if (!hasSignalInput && !hasSignalOutput) {
+ // Probably a sink, like a display.
+ //
+ // There are not any cases where a UI block
+ // generates data that doesn't have a trigger-like
+ // function.
+ return 'capture';
+ }
+
+ if (hasSignalInput) {
+ // Operation, like a counter.
+ // The cases where this should be used for a UI block are not clear.
+ return 'stop';
+ }
+ }
+ else {
+ throw new Error(`Unexpected: Direct value block should not have an input`);
+ }
+
+ throw new Error(`Unexpected signal properties: ${JSON.stringify(node.data)} | ${isUiFlowBlockData(node.data)}`)
+
+ });
+
+ if (!scan_downstream(graph, block_id, conn_index, find_triggers_downstream_controller)) {
+ return false; // Ignore this entry if id doesn't derive into a trigger
+ }
+ }
+
+ return true;
+ }
+ else {
+ return false;
+ }
+}
+
+export function get_unreachable(graph: FlowGraph): string[] {
+ const reached = build_index(get_source_signals(graph));
+
+ let remaining_connections: FlowGraphEdge[] = [].concat(graph.edges);
+ let empty_pass = false;
+
+ // Propagate signals forward to check which operation blocks are reachable
+ do {
+ empty_pass = true;
+
+ const skipped: FlowGraphEdge[] = [];
+ for (const conn of remaining_connections) {
+ if (reached[conn.from.id]) {
+ // Connection activated
+ if (makes_reachable(conn, graph.nodes[conn.to.id])) {
+ empty_pass = false;
+ reached[conn.to.id] = true;
+ }
+ }
+ else {
+ skipped.push(conn);
+ }
+ }
+
+ remaining_connections = skipped;
+ } while (!empty_pass);
+
+ // Propagate remaining signals back to see which getters and value nodes are left unused
+ do {
+ empty_pass = true;
+
+ const skipped: FlowGraphEdge[] = [];
+ for (const conn of remaining_connections) {
+ if (reached[conn.to.id]) {
+ // Connection activated
+ if (is_getter_node(graph.nodes[conn.from.id])) {
+ empty_pass = false;
+ reached[conn.from.id] = true;
+ }
+ }
+ else {
+ skipped.push(conn);
+ }
+ }
+
+ remaining_connections = skipped;
+ } while (!empty_pass);
+
+ return set_difference(Object.keys(graph.nodes), reached);
+}
+
+export function get_source_signals(graph: FlowGraph): string[] {
+ const signals = [];
+
+ const conn_index = index_connections(graph);
+ const rev_conn_index = reverse_index_connections(graph);
+
+ for (const block_id of Object.keys(graph.nodes)) {
+ if (is_trigger_node(graph, block_id, conn_index, rev_conn_index)) {
+ signals.push(block_id);
+ }
+ }
+
+
+ return signals;
+}
+
+function has_pulse_output(block: FlowGraphNode): boolean {
+ if (isAtomicFlowBlockData(block.data)){
+ const outputs = block.data.value.options.outputs || [];
+
+ return outputs.filter(v => is_pulse(v)).length > 0;
+ }
+ else if (isDirectValueBlockData(block.data)){
+ const data = block.data as DirectValueFlowBlockData;
+
+ return is_pulse(data.value);
+ }
+ else if (isEnumDirectValueBlockData(block.data)){
+ return false;
+ }
+ else if (isUiFlowBlockData(block.data)){
+ const outputs = block.data.value.options.outputs || [];
+
+ return outputs.filter(v => is_pulse(v)).length > 0;
+ }
+ else {
+ throw new Error("Unknown block type: " + block.data.type)
+ }
+}
+
+function has_pulse_input(block: FlowGraphNode): boolean {
+ if (isAtomicFlowBlockData(block.data)){
+ const data = block.data as AtomicFlowBlockData;
+
+ const inputs = data.value.options.inputs || [];
+
+ return inputs.filter(v => is_pulse(v)).length > 0;
+ }
+ else if (isDirectValueBlockData(block.data)){
+ return false;
+ }
+ else if (isEnumDirectValueBlockData(block.data)){
+ return false;
+ }
+ else if (isUiFlowBlockData(block.data)){
+ const inputs = block.data.value.options.inputs || [];
+
+ return inputs.filter(v => is_pulse(v)).length > 0;
+ }
+ else {
+ throw new Error("Unknown block type: " + block.data.type)
+ }
+}
+
+function is_node_conversor_streaming_to_step(node: FlowGraphNode): boolean {
+ if (has_pulse_output(node) && !has_pulse_input(node)) {
+ return true;
+ }
+
+ if (isUiFlowBlockData(node.data)) {
+ return true;
+ }
+
+ return false;
+}
+
+export function get_conversions_to_stepped(graph: FlowGraph, source_block_id: string): string[] {
+ const results: {[key: string]: boolean} = {};
+ const reached = build_index([source_block_id]);
+
+ let remaining_connections: FlowGraphEdge[] = [].concat(graph.edges);
+ let empty_pass = false;
+
+ // If the source node feeds into a non getter, it should also be considered as a conversor
+ const source_block = graph.nodes[source_block_id];
+ if (is_node_conversor_streaming_to_step(source_block)) {
+ for (const conn of remaining_connections) {
+ if (conn.from.id === source_block_id) {
+ const target = graph.nodes[conn.to.id];
+ if (has_pulse_input(target)) {
+ results[source_block_id] = true;
+ break;
+ }
+ }
+ }
+ }
+
+ do {
+ empty_pass = true;
+
+ const skipped: FlowGraphEdge[] = [];
+ for (const conn of remaining_connections) {
+ if (reached[conn.from.id]) {
+ const node = graph.nodes[conn.to.id];
+ if (is_node_conversor_streaming_to_step(node)) {
+ // Conversor to step
+ results[conn.to.id] = true;
+ }
+ else {
+ // Part of the streaming flow
+ empty_pass = false;
+ reached[conn.to.id] = true;
+ }
+ }
+ else {
+ skipped.push(conn);
+ }
+ }
+
+ remaining_connections = skipped;
+ } while (!empty_pass);
+
+ return Object.keys(results);
+}
+
+export function get_pulse_continuations(graph: FlowGraph, source_id: string): FlowGraphEdge[][] {
+ const outputs: {[key: string]: FlowGraphEdge}[] = [];
+
+ const block = graph.nodes[source_id];
+
+ for (const conn of graph.edges) {
+ if (conn.from.id === source_id) {
+ if (is_pulse_output(block, conn.from.output_index)) {
+ if (!outputs[conn.from.output_index]) {
+ outputs[conn.from.output_index] = {};
+ }
+ outputs[conn.from.output_index][conn.to.input_index + '_' + conn.to.id] = conn;
+ }
+ }
+ }
+
+ return outputs.map(conn_set => Object.values(conn_set));
+}
+
+export interface BlockTreeArgument {
+ tree: BlockTree, output_index: number
+};
+
+export interface BlockTree {
+ block_id: string,
+ arguments: BlockTreeArgument[],
+};
+
+interface BlockTreeOutputValue {
+ block: BlockTree;
+ output_index: number;
+};
+
+export interface SteppedBlockTreeBlock extends BlockTree {
+ contents: SteppedBlockTree[],
+};
+
+export interface VirtualSteppedBlock {
+ block_id: string,
+ type: string,
+ contents?: (SteppedBlockTree | SteppedBlockTree[])[],
+ arguments?: BlockTreeArgument[],
+}
+
+export interface SteppedBlockTreeJump extends VirtualSteppedBlock {
+ block_id: string,
+ type: 'jump_to_block';
+}
+
+export interface SteppedBlockTreeFork extends VirtualSteppedBlock {
+ block_id: string,
+ type: 'op_fork_execution',
+ contents: (SteppedBlockTree | SteppedBlockTree[])[],
+}
+
+export type SteppedBlockTree = SteppedBlockTreeBlock | VirtualSteppedBlock;
+
+export function get_tree_with_ends(graph: FlowGraph, top: string, bottom: string): BlockTree {
+ const args: FlowGraphEdge[] = [];
+
+ for (const conn of graph.edges) {
+ if (conn.to.id === bottom) {
+ if (args[conn.to.input_index] !== undefined) {
+ throw new Error("Multiple inputs on single port");
+ }
+
+ args[conn.to.input_index] = conn;
+ }
+ }
+
+ return {
+ block_id: bottom,
+ arguments: args.map(conn => {
+ if (conn) {
+ return {
+ tree: get_tree_with_ends(graph, top, conn.from.id),
+ output_index: conn.from.output_index,
+ };
+ }
+ else {
+ return null;
+ }
+ })
+ };
+}
+
+export function get_filters(graph: FlowGraph, source_block_id: string): BlockTree[] {
+ const conversions = get_conversions_to_stepped(graph, source_block_id);
+
+ return conversions.map((bottom_id) => {
+ if (bottom_id === source_block_id) {
+ // Although the source performs the conversion, there's no filter.
+ return null;
+ }
+ return get_tree_with_ends(graph, source_block_id, bottom_id)
+ });
+}
+
+export function get_stepped_block_arguments(graph: FlowGraph, block_id: string,
+ source_id: string,
+ conn_index: EdgeIndex,
+ rev_conn_index: EdgeIndex,
+ ): BlockTreeArgument[] {
+ const args: BlockTreeArgument[] = [];
+
+ let pulse_offset = 0;
+ let pulse_ports: {[key: string]: boolean} = {};
+
+ const arg_conns: IndexedFlowGraphEdge[][] = [];
+
+ for (const conn of rev_conn_index[block_id] || []) {
+ if (is_pulse_output(graph.nodes[conn.from.id], conn.from.output_index)) {
+ pulse_ports[conn.to.input_index] = true;
+ pulse_offset = Math.max(pulse_offset, conn.to.input_index + 1);
+ }
+ else {
+ const idx = conn.to.input_index;
+ pulse_ports[idx] = false;
+
+ if (!arg_conns[idx]) {
+ arg_conns[idx] = [];
+ }
+ arg_conns[idx].push(conn);
+ }
+ }
+
+ for (let idx = 0; idx < arg_conns.length;idx++) {
+
+ if (!arg_conns[idx]) {
+ continue;
+ }
+
+ let conn = arg_conns[idx][0];
+ if (arg_conns[idx].length > 1) {
+ // If multiple connections, take the one from source_id, if not found, that's an error.
+ conn = arg_conns[idx].find(c => c.from.id === source_id);
+
+ if (!conn) {
+ throw new Error("Multiple inputs on single port."
+ + " This is only allowed if the inputs correspond to the different triggers, but it's not the case here.");
+ }
+ }
+
+ args[conn.to.input_index] = {
+ tree: {
+ block_id: conn.from.id,
+ arguments: get_stepped_block_arguments(graph, conn.from.id, source_id, conn_index, rev_conn_index),
+ },
+ output_index: conn.from.output_index,
+ };
+ }
+
+ // Validate that all pulse's are grouped
+ for (let i = 0; i < pulse_offset; i++) {
+ if (pulse_ports[i]) {
+ args.shift();
+ }
+ else {
+ throw new Error(`Non-pulse input before a pulse one on block_id:${block_id} (Port: ${pulse_offset}. Block: ${JSON.stringify(graph.nodes[block_id].data)})`);
+ }
+ }
+
+ return args;
+}
+
+function get_stepped_ast_continuation(graph: FlowGraph,
+ continuation: FlowGraphEdge,
+ source_id: string,
+ conn_index: EdgeIndex,
+ rev_conn_index: EdgeIndex,
+ ast: SteppedBlockTree[],
+ reached: {[key: string]: boolean}) {
+
+ const block_id = continuation.to.id;
+
+ if (reached[block_id]) {
+ // Create new jump-to block here
+
+ ast.push({
+ block_id: block_id,
+ type: JUMP_TO_BLOCK_OPERATION
+ });
+ }
+ else {
+ ast.push({
+ block_id: block_id,
+ arguments: get_stepped_block_arguments(graph, block_id, source_id, conn_index, rev_conn_index),
+ contents: [],
+ });
+
+ reached[block_id] = true;
+ get_stepped_ast_branch(graph, block_id, ast, reached);
+ }
+}
+
+function cut_on_block_id(ast: SteppedBlockTree[], block_id: string): [SteppedBlockTree[], SteppedBlockTree[]] {
+ for (let cut_idx = 0;cut_idx < ast.length; cut_idx++) {
+ if (ast[cut_idx].block_id === block_id) {
+ return [
+ ast.slice(0, cut_idx),
+ ast.slice(cut_idx),
+ ];
+ }
+ }
+
+ // If the block is not found, the merge point is not in scope
+ return [
+ ast,
+ [],
+ ];
+}
+
+function find_common_merge(asts: SteppedBlockTree[][], options: { prune_not_finishing: boolean }): { asts: SteppedBlockTree[][], common_suffix: SteppedBlockTree[] } {
+ const findings: { [key: string]: number[]} = {};
+ const common_blocks: [string, number][] = [];
+
+ if (asts.length === 0) {
+ return null;
+ }
+
+ let not_finishing = [];
+ if (options && options.prune_not_finishing) {
+ for (let idx = 0; idx < asts.length; idx++) {
+ const ast = asts[idx];
+
+ for (let op_idx = 0; op_idx < ast.length; op_idx++) {
+ const op = ast[op_idx];
+
+ if ((op as VirtualSteppedBlock).type) {
+ const fun = (op as VirtualSteppedBlock).type;
+
+ if (fun === JUMP_TO_BLOCK_OPERATION || fun === JUMP_TO_POSITION_OPERATION) {
+ not_finishing.push(idx);
+ break;
+ }
+ }
+ }
+ }
+ if (not_finishing.length === asts.length) {
+ not_finishing = [];
+ }
+ }
+
+
+ for (let idx = 0; idx < asts.length; idx++) {
+ const ast = asts[idx];
+
+ for (let op_idx = 0; op_idx < ast.length; op_idx++) {
+ const op = ast[op_idx];
+
+ const block_id = op.block_id;
+ if (!findings[block_id]) {
+ findings[block_id] = [];
+ }
+
+ findings[block_id].push(op_idx);
+ if ((findings[block_id].length + not_finishing.length) === asts.length) {
+ common_blocks.push([block_id, findings[block_id].reduce((a, b) => a + b, 0)]);
+ }
+ else if (findings[block_id].length > asts.length) {
+ throw new Error(`Duplicated block (id=${block_id}) found on ast`);
+ }
+ }
+ }
+
+ if (common_blocks.length === 0) {
+ return null;
+ }
+
+ const sorted_ascending = common_blocks.sort((a, b) => a[1] - b[1]);
+ const first_cut_id = sorted_ascending[0][0];
+
+ let common_suffix = null;
+ const differences = [];
+ for (let idx = 0; idx < asts.length; idx++) {
+ if (not_finishing.indexOf(idx) >= 0) {
+ differences.push(asts[idx]);
+ }
+ else {
+ const [unique_ast, suffix] = cut_on_block_id(asts[idx], first_cut_id);
+ if (common_suffix === null) {
+ common_suffix = suffix;
+ }
+ differences.push(unique_ast);
+ }
+ }
+
+ if (common_suffix === null) {
+ throw new Error('Unexpected: No suffix, but it should have.');
+ }
+
+ return {
+ asts: differences,
+ common_suffix: common_suffix,
+ }
+}
+
+function find_common_merge_groups_ast(asts: SteppedBlockTree[][],
+ options: { prune_not_finishing: boolean, remove_empty: boolean }
+ ): SteppedBlockTree[][] {
+
+ const findings: { [key: string]: [number, number][]} = {};
+ const common_blocks: {[key:string]: [number[], number]} = {};
+ const grouped: {[key: string]: boolean} = {};
+
+ if (asts.length === 0) {
+ return null;
+ }
+
+ for (let idx = 0; idx < asts.length; idx++) {
+ const ast = asts[idx];
+
+ // Remove empty ASTs if so requested (used on Fork() blocks).
+ if ((ast.length === 0) && options.remove_empty) {
+ asts.splice(idx, 1);
+ idx--;
+ continue;
+ }
+
+ const found_blocks: {[key: string]: boolean} = {};
+
+ for (let op_idx = 0; op_idx < ast.length; op_idx++) {
+ const op = ast[op_idx];
+
+ const block_id = op.block_id;
+ // Stop if already found by this same column
+ // This means a there's a loop inside the fork
+ if (found_blocks[block_id]) {
+ break;
+ }
+ found_blocks[block_id] = true;
+
+ if (!findings[block_id]) {
+ findings[block_id] = [];
+ }
+
+ findings[block_id].push([idx, op_idx]);
+ const block_findings = findings[block_id];
+
+ if (block_findings.length > 1) {
+ grouped[idx] = true;
+ common_blocks[block_id] = [block_findings.map(v => v[0]),
+ block_findings.reduce((acc, val) => acc + val[1], 0)];
+ }
+ if (block_findings.length === 2) {
+ // Add also the first on the list
+ grouped[block_findings[0][0]] = true;
+ }
+ }
+ }
+
+ if (Object.keys(grouped).length === 0) {
+ return null; // No groups found
+ }
+
+ if (Object.keys(common_blocks).length === 0) {
+ throw new Error('This should not happen. Groups found but no common_blocks');
+ }
+
+ const groups: number[][] = [];
+ const group_index: {[key: string]: boolean} = {};
+ for (const block of Object.values(common_blocks)) {
+ if (!group_index[block[0].toString()]) {
+ group_index[block[0].toString()] = true;
+ groups.push(block[0]);
+ }
+ }
+
+ groups.sort((x, y) => y.length - x.length); // Descending length
+
+ const group_asts: { asts: SteppedBlockTree[][], common_suffix: SteppedBlockTree[] }[] = [];
+ const in_previous_group: {[key: number]: [number, number]} = {};
+
+ // Build group tree
+ for (let group_idx = 0; group_idx < groups.length; group_idx++) {
+ const column_asts = [];
+ const inserts = [];
+ const deletes = [] ;
+ let ast_idx = -1;
+ const inserted_asts: {[key: string]: boolean} = {};
+ for (const column of groups[group_idx]) {
+ ast_idx++;
+
+ if (in_previous_group[column] !== undefined) {
+ const prev_group = in_previous_group[column];
+ const splitted_ast = group_asts[prev_group[0]];
+
+ if (!inserted_asts[prev_group[0]]) {
+ inserts.push(prev_group[0]);
+ }
+ inserted_asts[prev_group[0]] = true;
+
+ column_asts.push(splitted_ast.asts[prev_group[1]]);
+ deletes.push({ group: prev_group[0], position: prev_group[1] })
+
+ in_previous_group[column] = [group_idx, ast_idx];
+ }
+ else {
+ column_asts.push(asts[column]);
+ in_previous_group[column] = [group_idx, ast_idx];
+ }
+ }
+
+ const merged_ast = find_common_merge(column_asts, options);
+ if (!merged_ast){
+ throw new Error(`Error merging asts (idx:${groups[group_idx]})`)
+ }
+
+ for (const _delete of deletes) {
+ delete group_asts[_delete.group].asts[_delete.position];
+ }
+
+ if (inserts.length === 0) {
+ group_asts.push(merged_ast);
+ }
+ else {
+ for (const insert of inserts) {
+ group_asts[insert].asts.push(merged_ast as any);
+ }
+ }
+ }
+
+ const grouped_ast_tree: (SteppedBlockTree[] | { asts: SteppedBlockTree[][], common_suffix: SteppedBlockTree[] })[] = group_asts;
+
+ for (let idx = 0; idx < asts.length; idx++) {
+ if (!grouped[idx]) {
+ grouped_ast_tree.push(asts[idx]);
+ }
+ }
+
+ // Clean grouped AST tree
+ const result: SteppedBlockTree[][] = [];
+
+ function compile_group(e: (SteppedBlockTree[] | { asts: SteppedBlockTree[][],
+ common_suffix: SteppedBlockTree[] })): SteppedBlockTree[] {
+ // Just copy non-grouped ASTs
+ if (!(e as any).common_suffix) {
+ return e as SteppedBlockTree[];
+ }
+
+ const group = e as { asts: SteppedBlockTree[][], common_suffix: SteppedBlockTree[] };
+
+ const fork_ref = uuidv4();
+
+ let commons: SteppedBlockTree[] = [];
+ for (const common of group.common_suffix) {
+ if (common !== undefined) {
+ commons = commons.concat(compile_group(common as any));
+ }
+ }
+
+ const contents: SteppedBlockTree[][] = [];
+ for (const content of group.asts) {
+ if (content !== undefined) {
+ contents.push(compile_group(content));
+ }
+ }
+
+ const fork_block: SteppedBlockTreeFork = {
+ block_id: fork_ref,
+ type: FORK_OPERATION,
+ arguments: [],
+ contents: contents,
+ };
+ return (([fork_block] as SteppedBlockTree[]).concat(commons));
+
+ }
+
+ for (const group of grouped_ast_tree) {
+ if (group !== undefined) {
+ result.push(compile_group(group));
+ }
+ }
+
+ return result;
+}
+
+function get_stepped_ast_branch(graph: FlowGraph, source_id: string, ast: SteppedBlockTree[], reached: {[key: string]: boolean}) {
+ let continuations = get_pulse_continuations(graph, source_id);
+
+ const conn_index = index_connections(graph);
+ const rev_conn_index = reverse_index_connections(graph);
+
+ if (continuations.length > 1) {
+ let contents: any /*: (SteppedBlockTree | SteppedBlockTree[])[]*/ = [];
+
+ for (const cont of continuations) {
+ const subast: SteppedBlockTree[] = [];
+
+ if (!cont || cont.length === 0) {
+ contents.push([]);
+ }
+ else if (cont.length > 1) {
+ throw new Error(`There should be one and only one pulse per output`)
+ }
+ else {
+ const subreached = Object.assign({}, reached)
+ get_stepped_ast_continuation(graph, cont[0], source_id, conn_index, rev_conn_index, subast, subreached);
+ contents.push(subast);
+ }
+ }
+
+ // Pruning has to be done on if-else constructions to properly handle loops
+ let prune_not_finishing = false;
+ const node = graph.nodes[source_id];
+ if (node.data.type === AtomicFlowBlock.GetBlockType()) {
+ const fun = (node.data as AtomicFlowBlockData).value.options.block_function;
+ const functions_requiring_prune = ['control_if_else'];
+
+ prune_not_finishing = functions_requiring_prune.indexOf(fun) >= 0;
+ }
+
+ let common_suffix: SteppedBlockTree[] = [];
+ const source_node = graph.nodes[source_id];
+ if (source_node.data.type === AtomicFlowBlock.GetBlockType() &&
+ (source_node.data as AtomicFlowBlockData).value.options.block_function === FORK_OPERATION) {
+
+ const merged_groups = find_common_merge_groups_ast(contents, { prune_not_finishing: false, remove_empty: true });
+ if (merged_groups) {
+ if (merged_groups.length === 1){
+ // There's a top level fork, just replace it
+ const fork_block = merged_groups[0][0];
+ if ((fork_block as VirtualSteppedBlock).type !== FORK_OPERATION) {
+ throw new Error(`Unexpected: Expecting first block of common merge to be ${FORK_OPERATION},`
+ + ` found ${(fork_block as VirtualSteppedBlock).type}`);
+ }
+
+ contents = fork_block.contents;
+ common_suffix = merged_groups[0].slice(1);
+ }
+ else {
+ contents = merged_groups;
+ common_suffix = [];
+ }
+ }
+ }
+ else if (source_node.data.type === AtomicFlowBlock.GetBlockType() &&
+ (source_node.data as AtomicFlowBlockData).value.options.block_function === REPEAT_OPERATION) {
+ // Set everything in place for REPEAT blocks
+
+ common_suffix = contents[2];
+ // contents[1] is ignored as it's not a pulse output
+ contents = contents[0];
+ }
+ else {
+ const re_merge_data = find_common_merge(contents, { prune_not_finishing });
+ if (re_merge_data) {
+ contents = re_merge_data.asts;
+ common_suffix = re_merge_data.common_suffix;
+ }
+ }
+
+
+ ast[ast.length - 1].contents = contents; // Update parent block contents
+ for (const op of common_suffix) {
+ ast.push(op);
+ }
+ }
+
+ if (continuations.length == 1) {
+ if (continuations[0].length == 1) {
+ const block = graph.nodes[source_id];
+ const is_atomic_block = isAtomicFlowBlockData(block.data);
+ let atom_data: AtomicFlowBlockData = null;
+ let func_name: string = null;
+ if (is_atomic_block) {
+ atom_data = block.data as AtomicFlowBlockData;
+ func_name = atom_data.value.options.block_function;
+ }
+
+ if (is_atomic_block && func_name === 'control_if_else') {
+ // IF statement with only one output
+ const contents: SteppedBlockTree[] = [];
+ get_stepped_ast_continuation(graph, continuations[0][0], source_id, conn_index, rev_conn_index, contents, reached);
+
+ ast[ast.length - 1].contents = [contents, []]; // Update parent block contents
+ }
+ else {
+ get_stepped_ast_continuation(graph, continuations[0][0], source_id, conn_index, rev_conn_index, ast, reached);
+ }
+ }
+ else {
+ throw new Error(`Multiple outputs pulses from the same port`);
+ }
+ }
+ else if (continuations.length == 0) {
+ // Empty AST
+ // Nothing to do
+ }
+}
+
+export function get_stepped_ast(graph: FlowGraph, source_id: string): SteppedBlockTree[] {
+ const result: SteppedBlockTree[] = [];
+
+ get_stepped_ast_branch(graph, source_id, result, {source_id: true})
+
+ return result;
+}
+
+function compile_contents(graph: FlowGraph, contents: SteppedBlockTree[]): CompiledBlock[] {
+ let latest = null;
+ const results = [];
+
+ for (const block of contents) {
+ const compiled = compile_block(graph, block.block_id, block.arguments, block.contents as SteppedBlockTree[],
+ { inside_args: false, orig_tree: block },
+ { before: latest }
+ )
+ if (compiled !== null) {
+ results.push(compiled);
+ latest = compiled;
+ }
+ }
+
+ return results;
+}
+
+
+function already_used_in_past(graph: FlowGraph, arg_id: string,
+ parent: string,
+ reference_block: string): boolean {
+
+ // TODO: Don't re-index every time this function is called
+ const rev_conn_index = reverse_index_connections(graph);
+
+ let has_pulse_input = false;
+ const node = graph.nodes[reference_block];
+ if (node.data.type === AtomicFlowBlock.GetBlockType()) {
+ const ref_node = node.data as AtomicFlowBlockData;
+
+ if (ref_node.value.options.type === 'operation') {
+ has_pulse_input = true;
+ }
+ else if (ref_node.value.options.type === 'trigger') {
+ // Trigger have no pulse inputs, but consider the signals to be cached
+ // TODO: Don't find source blocks each time this function is called
+ const sources = get_source_signals(graph);
+ if (sources.indexOf(arg_id) >= 0) {
+ return true;
+ }
+ }
+ }
+
+ for (const conn of rev_conn_index[reference_block]) {
+ const is_pulse_connection = is_pulse_output(graph.nodes[conn.from.id], conn.from.output_index);
+ if (has_pulse_input && !is_pulse_connection) {
+ // Skip first level non-pulse outputs if the reference block has any
+ // pulse input
+ continue;
+ }
+
+ // Consider direct pulse outputs (mostly from triggers)
+ if (is_pulse_connection) {
+ if (conn.from.id === arg_id) {
+ return true;
+ }
+ }
+
+ const result = (scan_upstream(graph, conn.from.id, rev_conn_index,
+ (found_id: string, _, path: string[]) => {
+ if (found_id === arg_id) {
+ if (path[path.length - 2] === parent) {
+ // Ignore this match and branch if it's just above the parent
+ return 'stop';
+ }
+ return 'capture';
+ }
+ else {
+ return 'continue';
+ }
+ }));
+ if (result) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function compile_arg(graph: FlowGraph, arg: BlockTreeArgument, parent: string, orig: string, arg_index: number, before: CompiledBlock | null): CompiledBlockArg {
+ const block = graph.nodes[arg.tree.block_id];
+
+ if (isAtomicFlowBlockData(block.data)){
+ if (block.data.value.options.block_function === 'op_on_block_run') {
+ const values = arg.tree.arguments.map(arg => graph.nodes[arg.tree.block_id].data);
+ if ((values.length != 2)
+ || !isDirectValueBlockData(values[0])
+ || !isDirectValueBlockData(values[1])) {
+
+ throw new Error(`Expected 2 direct values for op_on_block_run, found: ${JSON.stringify(values)}`)
+ }
+
+ return {
+ type: 'block',
+ value: [
+ {
+ type: 'flow_last_value',
+ contents: [],
+ args: [
+ {
+ type: 'constant',
+ value: values[0].value.value,
+ },
+ {
+ type: 'constant',
+ value: values[1].value.value,
+ },
+ ],
+ }
+ ]
+ }
+ }
+ if (already_used_in_past(graph, arg.tree.block_id, parent, orig)) {
+ return {
+ type: 'block',
+ value: [
+ {
+ type: 'flow_last_value',
+ contents: [],
+ args: [
+ {
+ type: 'constant',
+ value: arg.tree.block_id,
+ },
+ {
+ type: 'constant',
+ value: arg.output_index,
+ }
+ ]
+ }
+ ]
+ }
+ }
+ else{
+ return {
+ type: 'block',
+ value: [ compile_block(graph, arg.tree.block_id, arg.tree.arguments, [],
+ { inside_args: true, orig_tree: null, arg_index: arg_index },
+ { before: before, exec_orig: orig },
+ ) ]
+ }
+ }
+ }
+ else if (isDirectValueBlockData(block.data)){
+ return {
+ type: 'constant',
+ value: block.data.value.value,
+ };
+ }
+ else if (isEnumDirectValueBlockData(block.data)){
+ let value = block.data.value.value_id;
+
+ const definition = block.data.value.options.definition;
+ if (definition && definition.type === 'enum_sequence') {
+ value = EnumDirectValue.cleanSequenceValue(value);
+ }
+
+ return {
+ type: 'constant',
+ value: value,
+ };
+ }
+ else if (isUiFlowBlockData(block.data)) {
+ return {
+ type: 'block',
+ value: [
+ {
+ type: 'data_ui_block_value',
+ args: [ {
+ type: 'constant',
+ value: arg.tree.block_id
+ } ],
+ contents: [],
+ }
+ ],
+ };
+ }
+ else {
+ throw new Error("Unknown block type: " + block.data.type)
+ }
+}
+
+function get_latest_blocks_on_each_ast_level(block: CompiledBlock): CompiledBlock[] {
+ const results: CompiledBlock[] = [];
+ const levels = [block.contents];
+
+ while (levels.length > 0) {
+ const next = levels.pop();
+ if (next.length === 0) {
+ continue;
+ }
+
+ if ((next[0] as CompiledBlock).type) {
+ // Are compiled blocks, so take the last and add to level
+ const latest = next[next.length - 1];
+ results.push(latest as CompiledBlock);
+ levels.push(latest.contents)
+ }
+ else {
+ for (const content of next){
+ levels.push(content.contents);
+ }
+ }
+ }
+
+ return results;
+}
+
+function compile_block(graph: FlowGraph,
+ block_id: string,
+ args: BlockTreeArgument[],
+ contents: SteppedBlockTree[][] | SteppedBlockTree[],
+ flags: { inside_args: boolean, orig_tree: SteppedBlockTree, arg_index?: number },
+ relatives: { before: CompiledBlock, exec_orig?: string }): CompiledBlock {
+
+ if (flags.orig_tree && (flags.orig_tree as VirtualSteppedBlock).type) {
+ const vblock = flags.orig_tree as VirtualSteppedBlock;
+
+ if (vblock.type === 'jump_to_block') {
+ return {
+ type: JUMP_TO_BLOCK_OPERATION,
+ args: [{ type: 'constant', value: block_id }],
+ contents: [],
+ };
+ }
+ else if (vblock.type === 'op_fork_execution') {
+ return {
+ type: FORK_OPERATION,
+ args: [],
+ contents: (contents as SteppedBlockTree[][]).map(c => { return { contents: compile_contents(graph, c)} })
+ };
+ }
+ else {
+ throw new Error(`Unknown virtual block (type: ${vblock.type})`);
+ }
+ }
+
+ const block = graph.nodes[block_id];
+
+ if (!block) {
+ if (flags) {
+ throw new Error(`Block not found (id: ${block_id}, inside_args: ${flags.inside_args})\nValue: ${JSON.stringify(flags.orig_tree)}`)
+ }
+ else {
+ throw new Error(`Block not found (id: ${block_id}, no flags)`)
+ }
+ }
+
+ if (isAtomicFlowBlockData(block.data)){
+ const data = block.data;
+
+ let compiled_contents: (CompiledBlock | ContentBlock)[] = [];
+ if (contents && contents.length) {
+ if (flags.inside_args) {
+ throw new Error("Found block with contents inside args");
+ }
+
+ if (contents.length > 0) {
+ if ((contents as SteppedBlockTree[][])[0] instanceof Array) {
+ compiled_contents = (contents as SteppedBlockTree[][]).map(v => {
+ return {
+ contents: compile_contents(graph, v)
+ }
+ })
+ }
+ else {
+ compiled_contents = compile_contents(graph, contents as SteppedBlockTree[]);
+ }
+ }
+ }
+
+ let block_type = null;
+ let compiled_args: CompiledBlockArgs = args.map(v => compile_arg(graph, v,
+ block_id,
+ relatives.exec_orig ? relatives.exec_orig : block_id,
+ v.output_index,
+ relatives.before));
+ const block_fun = data.value.options.block_function;
+ const slot_args: CompiledBlockArgList = [];
+
+ const slots = data.value.slots;
+
+ if (slots) {
+ for (const slot of Object.keys(slots)){
+ slot_args.push({ type: slot as any, value: slots[slot]})
+ }
+ }
+
+ // Prepend slots (sorted) to args
+ compiled_args = slot_args.concat(compiled_args);
+
+ // Only wait for time monitors if the same block has not been used before on the same flow
+ if (TIME_TRIGGERS.indexOf(block_fun) >= 0) {
+ const key = block_fun === 'flow_utc_time' ? 'utc_time' : 'utc_date';
+
+ let alreadyRun: boolean | null = null;
+ if (flags.inside_args) {
+ if (relatives.before && relatives.before.id === block_id) {
+ alreadyRun = true;
+ }
+ else if (relatives.before) {
+
+ // TODO: Don't re-index every time this function is called
+ const rev_conn_index = reverse_index_connections(graph);
+
+ const foundBefore = scan_upstream(graph, relatives.before.id, rev_conn_index,
+ (node_id: string, _node: FlowGraphNode) => {
+ if (node_id === block_id) {
+ return 'capture';
+ }
+ else {
+ return 'continue';
+ }
+ });
+
+ if (foundBefore) {
+ alreadyRun = true;
+ }
+ }
+ }
+
+ // Is not an arg (so, it's a signal) or it's a new block
+ if (!flags.inside_args) {
+ block_type = "wait_for_monitor";
+ compiled_args = {
+ monitor_id: {
+ from_service: TIME_MONITOR_ID,
+ },
+ key: key,
+ monitor_expected_value: 'any_value',
+ };
+ }
+ else if (!alreadyRun) {
+ block_type = "wait_for_monitor";
+ compiled_args = {
+ monitor_id: {
+ from_service: TIME_MONITOR_ID,
+ },
+ key: key,
+ monitor_expected_value: 'any_value',
+ monitored_value: flags.arg_index,
+ };
+ }
+ else {
+ return {
+ type: "flow_last_value",
+ args: [
+ {
+ type: 'constant',
+ value: block_id,
+ },
+ {
+ type: 'constant',
+ value: flags.arg_index,
+ }
+ ],
+ contents: [],
+ };
+ }
+ }
+ else if (block_fun.startsWith('services.')) {
+ if (data.value.options.type === 'trigger') {
+ // Ignore
+ block_type = block_fun;
+
+ if (flags.inside_args) {
+ return {
+ type: "flow_last_value",
+ args: [
+ {
+ type: 'constant',
+ value: block_id,
+ },
+ {
+ type: 'constant',
+ value: flags.arg_index,
+ }
+ ],
+ contents: [],
+ };
+ }
+ else {
+ const listenerArgs: {key: string, subkey?: any, monitor_expected_value?: any} = {
+ key: block_fun.split('.').reverse()[0],
+ };
+
+ if (data.value.options.key) {
+ listenerArgs.key = data.value.options.key;
+ }
+
+ const subkey = data.value.options.subkey;
+ if (subkey) {
+ listenerArgs.subkey = compiled_args[subkey.index - 1];
+ compiled_args.splice(subkey.index - 1, 1);
+ }
+
+ if (compiled_args.length > 0) {
+ listenerArgs.monitor_expected_value = compiled_args[0];
+ }
+
+ compiled_args = (listenerArgs as any);
+ }
+ }
+ else {
+ block_type = "command_call_service";
+ const [, bridge_id, call_name] = block_fun.split('.');
+ compiled_args = {
+ service_id: bridge_id,
+ service_action: call_name,
+ service_call_values: compiled_args,
+ };
+ }
+ }
+ else if (block_fun === 'trigger_when_all_true') {
+ block_type = "control_if_else";
+
+ // Tie arguments with an *and* operation
+ compiled_args = [{
+ type: 'block',
+ value: [{
+ type: "operator_and",
+ contents: [],
+ args: compiled_args,
+ }],
+ }];
+ }
+ else if (block_fun === 'trigger_when_first_completed') {
+ // Natural exit of an IF.
+ // This block is structural and MUST not appear on a compiled AST.
+ if (relatives.before && ['control_if_else', FORK_OPERATION].indexOf(relatives.before.type) < 0 ) {
+ throw new Error("Expected block 'trigger_when_first_completed' 'control_if_else' or 'op_fork_execution'");
+ }
+
+ if (relatives.before) {
+ const all_before = get_latest_blocks_on_each_ast_level(relatives.before).concat([relatives.before]);
+
+ for (const block of all_before) {
+ if (block.type === FORK_OPERATION) {
+ if (!block.args) {
+ block.args = [{
+ type: 'constant',
+ value: 'exit-when-first-completed'
+ }];
+ }
+ else if (Array.isArray(block.args)) {
+ if (block.args
+ .filter(a => a.type === 'constant'
+ && (a.value === 'exit-when-first-completed'
+ || a.value === 'exit-when-all-completed'))
+ .length === 0) {
+
+ block.args.push({
+ type: 'constant',
+ value: 'exit-when-first-completed'
+ });
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ else if (block_fun === 'trigger_when_all_completed') {
+ // Natural exit of a Fork
+ // This block is structural and MUST not appear on a compiled AST.
+ if (relatives.before && relatives.before.type === FORK_OPERATION ) {
+ // Nothing to add
+ const all_before = get_latest_blocks_on_each_ast_level(relatives.before).concat([relatives.before]);
+
+ for (const block of all_before) {
+ if (block.type === FORK_OPERATION) {
+ if (!block.args) {
+ block.args = [{
+ type: 'constant',
+ value: 'exit-when-all-completed'
+ }];
+ }
+ else if (Array.isArray(block.args)) {
+ if (block.args
+ .filter(a => a.type === 'constant'
+ && (a.value === 'exit-when-first-completed'
+ || a.value === 'exit-when-all-completed'))
+ .length === 0) {
+
+ block.args.push({
+ type: 'constant',
+ value: 'exit-when-all-completed'
+ });
+ }
+ }
+ }
+ }
+ }
+ else {
+ throw new Error("Expected block 'trigger_when_all_completed' after 'op_fork_execution'");
+ }
+ return null;
+ }
+ else if (BASE_TOOLBOX_BLOCKS[block_fun]) {
+ block_type = block_fun;
+ }
+ else {
+ throw new Error("Unknown block: " + block_fun);
+ }
+
+ return {
+ id: block_id,
+ type: block_type as CompiledBlockType,
+ args: compiled_args,
+ contents: compiled_contents,
+ report_state: block.data.value.report_state,
+ };
+ }
+ else if (isDirectValueBlockData(block.data)){
+ const data = block.data;
+
+ if (contents.length > 0) {
+ throw new Error("AssertionError: Contents.length > 0 in DirectValue block")
+ }
+
+ return {
+ type: 'constant',
+ value: data.value.value,
+
+ // Not really a compiled block, this would be an argument, but this simplifies things
+ } as any as CompiledBlock;
+ }
+ else if (isEnumDirectValueBlockData(block.data)){
+ const data = block.data as EnumDirectValueFlowBlockData;
+
+ if (contents.length > 0) {
+ throw new Error("AssertionError: Contents.length > 0 in EnumDirectValue block")
+ }
+
+ return {
+ type: 'constant',
+ value: data.value.value_id,
+
+ // Not really a compiled block, this would be an argument, but this simplifies things
+ } as any as CompiledBlock;
+ }
+ else if (isUiFlowBlockData(block.data)) {
+
+ const compiled_args: CompiledBlockArgs = args.map(v => compile_arg(graph, v,
+ block_id,
+ relatives.exec_orig ? relatives.exec_orig : block_id,
+ v.output_index,
+ relatives.before));
+
+ return {
+ id: block_id,
+ type: ('services.ui.' + block.data.value.options.id + '.' + block_id) as any,
+ args: compiled_args,
+ contents: [],
+ };
+
+ }
+ else {
+ throw new Error("Unknown block type: " + block.data.type)
+ }
+}
+
+type BlockPositionIndex = {[key: string]: number[]};
+
+function process_jump_index_on_contents(contents: (CompiledBlock | ContentBlock)[],
+ index: BlockPositionIndex,
+ prefix: number[]) {
+
+ for (let idx = 0; idx < contents.length; idx++) {
+ const op = contents[idx];
+
+ if ((op as CompiledBlock).type === 'jump_point') {
+ const block = (op as CompiledBlock);
+ index[(block.args as CompiledBlockArgList)[0].value] = prefix.concat([idx])
+
+ // Remove this operation
+ contents.splice(idx, 1);
+ idx--;
+
+ if (op.contents && op.contents.length) {
+ throw new Error('Jump point must not have contents');
+ }
+ }
+ else if ((op as CompiledBlock).id) {
+ const block = (op as CompiledBlock);
+ index[block.id] = prefix.concat([idx])
+ }
+ op.contents = process_jump_index_on_contents(op.contents || [], index, prefix.concat([idx]));
+ }
+
+ return contents;
+}
+
+function process_graph_jump_index(graph: CompiledFlowGraph): BlockPositionIndex {
+ const index: BlockPositionIndex = {};
+ process_jump_index_on_contents(graph, index, []);
+
+ return index;
+}
+
+function link_jumps(contents: (CompiledBlock | ContentBlock)[], positions: BlockPositionIndex) {
+ for (const op of contents) {
+
+ if ((op as CompiledBlock).type === JUMP_TO_BLOCK_OPERATION) {
+ const block = (op as CompiledBlock);
+
+ const link = (block.args as CompiledBlockArgList)[0].value as string;
+ block.type = JUMP_TO_POSITION_OPERATION;
+ if (!positions[link]) {
+ throw new Error(`Cannot link to "${link}"`)
+ }
+
+ (block.args as CompiledBlockArgList)[0].value = positions[link];
+ }
+
+ link_jumps(op.contents || [], positions);
+ }
+}
+
+export function _link_graph(graph: CompiledFlowGraph): CompiledFlowGraph {
+ const block_id_index = process_graph_jump_index(graph);
+
+ link_jumps(graph, block_id_index);
+
+ return graph;
+}
+
+function get_argument_sources(graph: FlowGraph, block: BlockTree, output_index: number): BlockTreeOutputValue[] {
+ const blockArgs = block.arguments.filter( arg => {
+ const node = graph.nodes[arg.tree.block_id];
+
+ if (isAtomicFlowBlockData(node.data) || isUiFlowBlockData(node.data)) {
+ return true;
+ }
+ else if (isDirectValueBlockData(node.data) || isEnumDirectValueBlockData(node.data)) {
+ return false;
+ }
+ else {
+ throw new Error("Unknown block type: " + node.data.type)
+ }
+ });
+
+ if (blockArgs.length === 0) {
+ return [{block, output_index}];
+ }
+
+ // This could be done with a flatMap, but that is fairly recent, so if we
+ // can increase the compatibility with a little boilerplate, it's not a bad
+ // tradeoff
+ let results: BlockTreeOutputValue[] = [];
+ for (const arg of blockArgs) {
+ results = results.concat(get_argument_sources(graph, arg.tree, arg.output_index));
+ }
+ return results;
+}
+
+function build_signal_from_source(graph: FlowGraph, source: BlockTreeOutputValue): CompiledBlock {
+ const source_node = graph.nodes[source.block.block_id];
+ if (isAtomicFlowBlockData(source_node.data)) {
+ const desc = source_node.data.value.options;
+ if (desc.block_function === 'data_variable') {
+ return {
+ id: source.block.block_id,
+ type: "on_data_variable_update",
+ args: [
+ {
+ type: "variable",
+ value: source_node.data.value.slots['variable'],
+ }
+ ],
+ contents: [],
+ }
+ }
+ else if (desc.block_function === 'data_itemoflist') {
+ return {
+ id: source.block.block_id,
+ type: "on_data_variable_update",
+ args: [
+ {
+ type: "variable",
+ value: source_node.data.value.slots['list'],
+ }
+ ],
+ contents: [],
+ }
+ }
+ else if (desc.type === 'operation') {
+ return {
+ type: "flow_last_value",
+ args: [
+ {
+ type: 'constant',
+ value: source.block.block_id,
+ },
+ {
+ type: 'constant',
+ value: source.output_index + '',
+ }
+ ]
+ }
+ }
+ else if (desc.block_function === 'op_on_block_run') {
+ const compiled_args: CompiledBlockArgs = source.block.arguments.map(v => compile_arg(graph, v,
+ source.block.block_id,
+ source.block.block_id,
+ v.output_index,
+ null, // Signal, so nothing before
+ ));
+
+ return {
+ id: source.block.block_id,
+ type: "op_on_block_run",
+ args: compiled_args,
+ contents: [],
+ }
+ }
+ else if (desc.block_function.startsWith('services.')) {
+ // Rewrite service calls to custom triggers
+ const compiled_args: CompiledBlockArgs = source.block.arguments.map(v => compile_arg(graph, v,
+ source.block.block_id,
+ source.block.block_id,
+ v.output_index,
+ null, // Signal, so nothing before
+ ));
+
+ const listenerArgs: {key: string, subkey?: any, monitor_expected_value?: any} = {
+ key: desc.block_function.split('.').reverse()[0],
+ };
+
+ if (desc.key) {
+ listenerArgs.key = desc.key;
+ }
+
+ const subkey = desc.subkey;
+ if (subkey) {
+ listenerArgs.subkey = compiled_args[subkey.index - 1];
+ compiled_args.splice(subkey.index - 1, 1);
+ }
+
+ if (compiled_args.length > 0) {
+ listenerArgs.monitor_expected_value = compiled_args[0];
+ }
+
+ return {
+ id: source.block.block_id,
+ type: (desc.block_function as any),
+ args: (listenerArgs as any),
+ contents: [],
+ }
+ }
+ else if (TIME_TRIGGERS.indexOf(desc.block_function) >= 0) {
+ const key = desc.block_function === 'flow_utc_time' ? 'utc_time' : 'utc_date';
+ return {
+ type: "wait_for_monitor",
+ args: {
+ monitor_id: {
+ from_service: TIME_MONITOR_ID,
+ },
+ key: key,
+ monitor_expected_value: 'any_value',
+ },
+ id: source.block.block_id,
+ }
+ }
+ else {
+ throw new Error(`Unexpected flow source node: (fun: ${desc.block_function}, type: ${source_node.data.type}, id: ${source.block.block_id})`);
+ }
+ }
+ else if (isUiFlowBlockData(source_node.data)) {
+ const compiled_args: CompiledBlockArgs = source.block.arguments.map(v => compile_arg(graph, v,
+ source.block.block_id,
+ source.block.block_id,
+ v.output_index,
+ null, // Signal, so nothing before
+ ));
+
+ return {
+ id: source.block.block_id,
+ type: ('services.ui.' + source_node.data.value.options.id + '.' + source.block.block_id) as any,
+ args: compiled_args,
+ contents: [],
+ };
+ }
+ else {
+ throw new Error(`Unexpected flow source node: (type: ${source_node.data.type}, id: ${source.block.block_id})`);
+ }
+}
+
+function build_stream_for_source(graph: FlowGraph,
+ source: BlockTreeOutputValue,
+ sink: BlockTree,
+ before: CompiledBlock
+ ): CompiledBlock[] {
+ const signal = build_signal_from_source(graph, source);
+
+ return [
+ signal,
+ compile_block(graph, sink.block_id, sink.arguments, [],
+ { inside_args: false, orig_tree: null },
+ { before: before }),
+ ];
+}
+
+function build_streaming_flow_to(graph: FlowGraph,
+ sink: BlockTree,
+ before: CompiledBlock,
+ ) : CompiledBlock[][] {
+
+ if (sink.arguments.length != 1) {
+ throw new Error("Not implemented `build_streaming_flow_to` for argument list length != 1");
+ }
+
+ const sources = get_argument_sources(graph, sink, null);
+ const programs = sources.map((source: BlockTreeOutputValue) => build_stream_for_source(graph, source, sink, before));
+
+ return programs;
+}
+
+export function assemble_flow(graph: FlowGraph,
+ signal_id: string,
+ filter: BlockTree | null,
+ stepped_ast: SteppedBlockTree[]): CompiledFlowGraph[] {
+
+ const conn_index = index_connections(graph);
+ const rev_conn_index = reverse_index_connections(graph);
+
+ const signal_ast = compile_block(graph, signal_id, get_stepped_block_arguments(graph, signal_id, null, conn_index, rev_conn_index), [],
+ { inside_args: false, orig_tree: null },
+ { before: null });
+
+ let skip_filter = false;
+ let compiled_graph: CompiledBlock[] = null;
+
+ if (filter) {
+ const filter_node = graph.nodes[filter.block_id];
+
+ if (isAtomicFlowBlockData(filter_node.data)) {
+ if (filter_node.data.value.options.block_function === 'trigger_on_signal') {
+ skip_filter = true;
+ }
+ }
+ else if (isUiFlowBlockData(filter_node.data)) {
+ if (!has_pulse_output(filter_node)) {
+ const flows = build_streaming_flow_to(graph, filter, signal_ast);
+ return flows.map(flow => _link_graph(flow));
+ }
+ }
+ else {
+ throw new Error(`Unexpected filter block: ${JSON.stringify(filter_node)}`);
+ }
+ }
+ else {
+ skip_filter = true;
+ }
+
+ if (skip_filter) {
+ const filter_ast = stepped_ast.map(b => compile_block(graph,
+ b.block_id,
+ b.arguments,
+ b.contents as SteppedBlockTree[],
+ { inside_args: false, orig_tree: null },
+ { before: signal_ast }
+ ));
+ compiled_graph = [
+ signal_ast,
+ ].concat(filter_ast);
+ }
+ else {
+ const filter_ast = compile_block(graph, filter.block_id, filter.arguments, [ stepped_ast, [] ],
+ { inside_args: false, orig_tree: null },
+ { before: signal_ast }
+ )
+
+ compiled_graph = [
+ signal_ast,
+ filter_ast,
+ ];
+ }
+
+ return [_link_graph(compiled_graph)];
+}
+
+function extract_non_arguments_from_block(startBlock: CompiledBlock): CompiledBlock[] {
+ // [DESTRUCTIVE]
+ //
+ // Finds blocks that call to functions that are not supported on arguments,
+ // replaces it by a `flow-last-value` and returns them to be moved.
+ const todo = [startBlock];
+ const extracted = [] as CompiledBlock[];
+
+ while (todo.length > 0) {
+ const block = todo.pop();
+
+ // Extraction is not needed for certain blocks, in these cases we'll
+ // perform the block property cleanup, but won't extract the blocks.
+ const skipExtraction = (block.type === 'control_wait_for_next_value');
+
+ // Args
+ let args = block.args;
+
+ if (!args) {
+ continue;
+ }
+
+ else if ((args as CompiledBlockArgMonitorDict).monitor_id) {
+ continue; // Nothing can be extracted here
+ }
+
+ else if ((args as CompiledBlockServiceCallSelectorArgs).key) {
+ continue; // Nothing can be extracted here
+ }
+
+ else if ((args as CompiledBlockArgCallServiceDict).service_call_values) {
+ // Look into the CompiledBlockArgList
+ args = (args as CompiledBlockArgCallServiceDict).service_call_values;
+ }
+
+ else if (!Array.isArray(args)) {
+ // Expected an Arg list, this is the one we will use and the only one left.
+ throw Error(`Unknown argument type: ${args}`);
+ }
+
+ const argList = args as CompiledBlockArgList;
+
+ for (const arg of argList) {
+ if (!arg) {
+ // Empty argument
+ continue;
+ }
+ else if (arg.type === 'constant') {
+ // Nothing to do here
+ continue;
+ }
+ else if (arg.type === 'variable' || arg.type === 'list') {
+ // Nothing to do here
+ continue;
+ }
+ else if (arg.type !== 'block') {
+ throw Error(`Unknown argument type: ${arg.type}`);
+ }
+
+ let idx = -1;
+ for (const block of (arg.value)) {
+ idx++;
+
+ todo.push(block);
+
+ // Extract required operations
+ const toExtract = (block.type === 'wait_for_monitor');
+ if (toExtract) {
+ let valueIdx = 0;
+ const monitorDict = (block.args as CompiledBlockArgMonitorDict);
+ if (monitorDict.monitored_value || monitorDict.monitored_value === 0) {
+ valueIdx = monitorDict.monitored_value;
+ delete monitorDict.monitored_value;
+ }
+
+ if (!skipExtraction) {
+ extracted.push(block);
+
+ if (!block.id) {
+ block.id = uuidv4();
+ }
+
+ arg.value[idx] = {
+ type: "flow_last_value",
+ args: [
+ {
+ type: 'constant',
+ value: block.id,
+ },
+ {
+ type: 'constant',
+ value: valueIdx,
+ }
+ ],
+ contents: [],
+ };
+ }
+ }
+ }
+ }
+ }
+
+ return extracted;
+}
+
+function extract_non_arguments_from_content(block: ContentBlock | CompiledBlock) {
+ // [DESTRUCTIVE]
+ //
+ // Calls `extract_non_arguments` for all the contents inside `block`.
+ const contents = block.contents;
+
+ if (contents.length == 0) {
+ return;
+ }
+
+ const sample = contents[0];
+ if ((sample as CompiledBlock).type) {
+ extract_non_arguments(contents as CompiledBlock[]);
+ }
+ else {
+ (contents as ContentBlock[]).forEach(content => extract_non_arguments_from_content(content));
+ }
+}
+
+function extract_non_arguments(flow: CompiledBlock[]) {
+ // [DESTRUCTIVE]
+ //
+ // Finds calls to operations that cannot be resolved immediately (like
+ // wait-for-monitor) that are done in arguments and DESTRUCTIVELY moves it
+ // to the operations before it would be used so.
+ let pos = 0;
+ while (pos < flow.length) {
+ const block = flow[pos];
+
+ extract_non_arguments_from_content(block);
+
+ const non_arguments = extract_non_arguments_from_block(block);
+
+ // Insert the arguments reversed on the flow
+ non_arguments.reverse();
+ flow.splice(pos, 0, ...non_arguments);
+
+ pos += non_arguments.length + 1;
+ }
+}
+
+export function compile(graph: FlowGraph): CompiledFlowGraph[] {
+ // Isolate destructive changes. These are mostly performed on
+ // `lift_common_ops` and `extract_internally_reused_arguments`.
+ graph = JSON.parse(JSON.stringify(graph));
+
+ graph = lift_common_ops(graph);
+ graph = extract_internally_reused_arguments(graph);
+ graph = split_streaming_after_stepped(graph);
+
+ const source_signals = get_source_signals(graph);
+ const filters: BlockTree[][] = [];
+ for (const signal_id of source_signals) {
+ const source_filters = get_filters(graph, signal_id);
+ filters.push(source_filters);
+ }
+
+ const stepped_asts = [];
+
+ for (let i = 0; i < filters.length ; i++) {
+ const subfilters = filters[i];
+ let columns = [];
+
+ if (subfilters.length > 0) {
+ for (const subfilter of subfilters) {
+ if (subfilter !== null) {
+ columns.push(get_stepped_ast(graph, subfilter.block_id));
+ }
+ else {
+ // If the source is a trigger, the filter does not exist itself
+ const ast = get_stepped_ast(graph, source_signals[i]);
+ if (ast.length > 0) {
+ // Skip triggers with no AST or filter
+ columns.push(ast);
+ }
+ }
+ }
+ }
+ else {
+ // If the source is a trigger, the filter does not exist itself
+ columns.push(get_stepped_ast(graph, source_signals[i]))
+ }
+ stepped_asts.push(columns);
+ }
+
+ // Finally assemble everything
+ let flows: CompiledFlowGraph[] = [];
+ for (let i = 0; i < source_signals.length; i++) {
+ const signal_id = source_signals[i];
+
+ if (filters[i].length > 0) {
+ for (let j = 0; j < filters[i].length; j++) {
+ const filter = filters[i][j];
+ const ast = stepped_asts[i][j];
+
+ if (ast) {
+ flows = flows.concat(assemble_flow(graph, signal_id, filter, ast));
+ }
+ }
+ }
+ else {
+ const ast = stepped_asts[i][0];
+ if (ast.length > 0) { // Ignore triggers with no operations
+ flows = flows.concat(assemble_flow(graph, signal_id, null, ast));
+ }
+ }
+ }
+
+ // Extract operations that cannot be done as arguments
+ flows.forEach(flow => extract_non_arguments(flow));
+
+ // TODO: Deduplicate programs
+
+ return flows;
+}
diff --git a/frontend/src/app/flow-editor/graph_transformations.ts b/frontend/src/app/flow-editor/graph_transformations.ts
new file mode 100644
index 00000000..1ed97d68
--- /dev/null
+++ b/frontend/src/app/flow-editor/graph_transformations.ts
@@ -0,0 +1,749 @@
+import { AtomicFlowBlock, AtomicFlowBlockData, isAtomicFlowBlockData } from './atomic_flow_block';
+import { FlowGraph, FlowGraphNode } from './flow_graph';
+import { index_connections, reverse_index_connections, IndexedFlowGraphEdge, EdgeIndex } from './graph_utils';
+import { DirectValue, DirectValueFlowBlockData, isDirectValueBlockData } from './direct_value';
+import { EnumDirectValue, isEnumDirectValueBlockData } from './enum_direct_value';
+import { uuidv4 } from './utils';
+import { OP_ON_BLOCK_RUN, OP_PRELOAD_BLOCK } from './base_toolbox_description';
+import { isUiFlowBlockData, UiFlowBlock, UiFlowBlockData } from './ui-blocks/ui_flow_block';
+import { MessageType } from './flow_block';
+
+export function is_pulse(x: { type: MessageType | 'enum' | 'enum_sequence'}): boolean {
+ return [ 'pulse', 'user-pulse' ].indexOf(x.type) >= 0;
+}
+
+function graph_scan_nodes(graph: FlowGraph, check: (node_id: string, node: FlowGraphNode) => boolean): string[] {
+ const results = [];
+ for (const node_id of Object.keys(graph.nodes)) {
+ if (check(node_id, graph.nodes[node_id])) {
+ results.push(node_id);
+ }
+ }
+
+ return results;
+}
+
+function graph_scan_atomic_nodes(graph: FlowGraph, check: (node_id: string, node: AtomicFlowBlockData) => boolean): string[] {
+ const atomic_block_type = AtomicFlowBlock.GetBlockType();
+
+ return graph_scan_nodes(graph, (node_id: string, node: FlowGraphNode) => {
+ if (node.data.type === atomic_block_type) {
+ return check(node_id, node.data as AtomicFlowBlockData);
+ }
+ else {
+ return false;
+ }
+ })
+}
+
+type FindCommand = 'continue' | 'stop' | 'capture';
+type ScanCommand = FindCommand;
+
+function get_paths_between(_graph: FlowGraph,
+ upper_id: string,
+ lower_id: string,
+ conn_index: {[key: string]: IndexedFlowGraphEdge[]}): [number, number][] {
+ const results: [number, number][] = [];
+
+ for (const top_conn of conn_index[upper_id] || []) {
+ const reached: {[key: string]: boolean} = {};
+ reached[upper_id] = true;
+
+ const aux = (descending_id: string, followed_conn_id: number, reached: {[key:string]: boolean}) => {
+ if (descending_id === lower_id) {
+ results.push([top_conn.index, followed_conn_id]);
+ return;
+ }
+
+ for (const conn of conn_index[descending_id] || []) {
+ if (reached[conn.to.id]) {
+ // Already explored, ignoring
+ continue;
+ }
+
+ reached[conn.to.id] = true;
+ aux(conn.to.id, conn.index, Object.assign({}, reached));
+ }
+ }
+
+ aux(top_conn.to.id, top_conn.index, reached);
+ }
+
+ return results;
+}
+
+export function find_downstream(graph: FlowGraph, source_id: string,
+ conn_index: EdgeIndex,
+ controller: (node_id: string, node: FlowGraphNode) => FindCommand): string[] {
+ const reached: {[key: string]: boolean} = {};
+ const results: {[key: string]: boolean} = {};
+ reached[source_id] = true;
+
+ const aux = (source_id: string) => {
+ for (const conn of conn_index[source_id] || []) {
+ const next_id = conn.to.id;
+ if (reached[next_id]) {
+ // Ignore repeated
+ continue;
+ }
+
+ reached[source_id] = true;
+
+ const command = controller(next_id, graph.nodes[next_id]);
+ if (command === 'continue') {
+ aux(next_id);
+ }
+ else if (command === 'stop') {
+ // Ignore
+ }
+ else if (command === 'capture') {
+ results[next_id] = true;
+ }
+ else {
+ throw new Error('Unknown "find" command: ' + command)
+ }
+ }
+ }
+
+ aux(source_id);
+ return Object.keys(results);
+}
+
+// Look for blocks upstream of a given one. Return all the ones that the 'controller' function 'capture's.
+export function find_upstream(graph: FlowGraph, source_id: string,
+ rev_conn_index: EdgeIndex,
+ controller: (node_id: string, node: FlowGraphNode) => FindCommand): string[] {
+ const reached: {[key: string]: boolean} = {};
+ const results: {[key: string]: boolean} = {};
+ reached[source_id] = true;
+
+ const aux = (source_id: string) => {
+ for (const conn of rev_conn_index[source_id] || []) {
+ const next_id = conn.from.id;
+ if (reached[next_id]) {
+ // Ignore repeated
+ continue;
+ }
+
+ reached[source_id] = true;
+
+ const command = controller(next_id, graph.nodes[next_id]);
+ if (command === 'continue') {
+ aux(next_id);
+ }
+ else if (command === 'stop') {
+ // Ignore
+ }
+ else if (command === 'capture') {
+ results[next_id] = true;
+ }
+ else {
+ throw new Error('Unknown "find" command: ' + command)
+ }
+ }
+ }
+
+ aux(source_id);
+ return Object.keys(results);
+}
+
+// Look for blocks upstream of a given one. At the first 'capture' return the
+// values of the path taken to reach the target.
+export function scan_upstream(graph: FlowGraph, source_id: string,
+ rev_conn_index: EdgeIndex,
+ controller: (node_id: string, node: FlowGraphNode, path: string[]) => ScanCommand): string[] {
+ const reached: {[key: string]: boolean} = {};
+ reached[source_id] = true;
+
+ const aux: ((source_id: string, path: string[]) => string[]) = (source_id: string, path: string[]) => {
+ for (const conn of rev_conn_index[source_id] || []) {
+ const next_id = conn.from.id;
+ if (reached[next_id]) {
+ // Ignore repeated
+ continue;
+ }
+
+ reached[source_id] = true;
+
+ const branch_path = path.concat([conn.from.id]);
+ const result = controller(next_id, graph.nodes[next_id], branch_path);
+ if (result === 'continue') {
+ let result = aux(next_id, branch_path);
+ if (result) {
+ return result;
+ }
+ }
+ else if (result === 'stop') {
+ // Ignore this branch
+ }
+ else if (result === 'capture') {
+ return branch_path;
+ }
+ else {
+ throw new Error('Unknown "scan" (upstream) command: ' + result);
+ }
+ }
+
+ }
+
+ return aux(source_id, [source_id]);
+}
+
+export function scan_downstream(graph: FlowGraph, source_id: string,
+ conn_index: EdgeIndex,
+ controller: (node_id: string, node: FlowGraphNode, path: string[]) => ScanCommand): string[] {
+ const reached: {[key: string]: boolean} = {};
+ reached[source_id] = true;
+
+ const aux = (source_id: string, path: string[]): string[] => {
+ for (const conn of conn_index[source_id] || []) {
+ const next_id = conn.to.id;
+ if (reached[next_id]) {
+ // Ignore repeated
+ continue;
+ }
+
+ reached[source_id] = true;
+
+ const branch_path = path.concat([conn.to.id]);
+ const result = controller(next_id, graph.nodes[next_id], branch_path);
+ if (result === 'continue') {
+ let result = aux(next_id, branch_path);
+ if (result) {
+ return result;
+ }
+ }
+ else if (result === 'stop') {
+ // Ignore this branch
+ }
+ else if (result === 'capture') {
+ return branch_path;
+ }
+ else {
+ throw new Error('Unknown "scan" (downstream) command: ' + result);
+ }
+ }
+
+ }
+
+ return aux(source_id, [source_id]);
+}
+
+export function is_pulse_output(block: FlowGraphNode, index: number): boolean {
+ if (block.data.type === AtomicFlowBlock.GetBlockType()){
+ const data = block.data as AtomicFlowBlockData;
+ if (!data.value.options) {
+ throw new Error(`No options found on ${JSON.stringify(block)}`)
+ }
+
+ const outputs = data.value.options.outputs;
+ if (!outputs[index]) {
+ throw new Error(`IndexError: Index (${index}) not found on outputs (${JSON.stringify(outputs)}). Block: ${JSON.stringify(block.data)}`)
+ }
+
+ // If it has no pulse inputs its a source block
+ return is_pulse(outputs[index]);
+ }
+ else if (block.data.type === DirectValue.GetBlockType()){
+ const data = block.data as DirectValueFlowBlockData;
+
+ return is_pulse(data.value);
+ }
+ else if (block.data.type === EnumDirectValue.GetBlockType()){
+ return false;
+ }
+ else if (block.data.type === UiFlowBlock.GetBlockType()){
+ const data = block.data as UiFlowBlockData;
+
+ if (!data.value.options) {
+ throw new Error(`No options found on ${JSON.stringify(block)}`)
+ }
+
+ const outputs = data.value.options.outputs;
+ if (!outputs[index]) {
+ throw new Error(`IndexError: Index (${index}) not found on outputs (${JSON.stringify(outputs)})`)
+ }
+
+ // If it has no pulse inputs its a source block
+ return is_pulse(outputs[index]);
+ }
+ else {
+ throw new Error("Unknown block type: " + block.data.type)
+ }
+}
+
+export function scan_pulse_upstream(graph: FlowGraph, source_id: string,
+ rev_conn_index: EdgeIndex,
+ controller: (node_id: string, node: FlowGraphNode, path: string[]) => ScanCommand): string[] {
+ const reached: {[key: string]: boolean} = {};
+ reached[source_id] = true;
+
+ const aux: ((source_id: string, path: string[]) => string[]) = (source_id: string, path: string[]) => {
+ for (const conn of rev_conn_index[source_id] || []) {
+ const next_id = conn.from.id;
+ if (reached[next_id] || (!is_pulse_output(graph.nodes[next_id], conn.from.output_index))) {
+ // Ignore repeated
+ continue;
+ }
+
+ reached[source_id] = true;
+
+ const branch_path = path.concat([conn.from.id]);
+ const result = controller(next_id, graph.nodes[next_id], branch_path);
+ if (result === 'continue') {
+ let result = aux(next_id, branch_path);
+ if (result) {
+ return result;
+ }
+ }
+ else if (result === 'stop') {
+ // Ignore this branch
+ }
+ else if (result === 'capture') {
+ return branch_path;
+ }
+ else {
+ throw new Error('Unknown "scan" (pulse-upstream) command: ' + result);
+ }
+ }
+
+ }
+
+ return aux(source_id, [source_id]);
+}
+
+function atomic_find_filter(controller: (node_id: string, node: AtomicFlowBlockData) => FindCommand
+ ): (node_id: string, node: FlowGraphNode) => FindCommand {
+ const atomic_block_type = AtomicFlowBlock.GetBlockType();
+
+ return (node_id: string, node: FlowGraphNode) => {
+ if (node.data.type === atomic_block_type) {
+ return controller(node_id, node.data as AtomicFlowBlockData);
+ }
+ else {
+ return 'continue'; // Ignore
+ }
+ }
+}
+
+function find_downstream_atomic(graph: FlowGraph, source_id: string,
+ conn_index: EdgeIndex,
+ controller: (node_id: string, node: AtomicFlowBlockData) => FindCommand): string[] {
+ return find_downstream(graph, source_id, conn_index, atomic_find_filter(controller));
+}
+
+function find_upstream_atomic(graph: FlowGraph, source_id: string,
+ rev_conn_index: EdgeIndex,
+ controller: (node_id: string, node: AtomicFlowBlockData) => FindCommand): string[] {
+ return find_upstream(graph, source_id, rev_conn_index, atomic_find_filter(controller));
+}
+
+function find_forks(graph: FlowGraph): string[] {
+ return graph_scan_atomic_nodes(graph, (_node_id: string, node: AtomicFlowBlockData) => {
+ return node.value.options.block_function === 'op_fork_execution';
+ });
+}
+
+function find_downstream_joins(graph: FlowGraph, source_id: string, conn_index: EdgeIndex): string[] {
+ return find_downstream_atomic(graph, source_id, conn_index,
+ (_node_id: string, node: AtomicFlowBlockData) => {
+ if (node.value.options.block_function === 'trigger_when_first_completed'
+ || node.value.options.block_function === 'trigger_when_all_completed') {
+ return 'capture';
+ }
+ else {
+ return 'continue';
+ }
+ });
+}
+
+function find_upstream_forks(graph: FlowGraph, source_id: string, rev_conn_index: EdgeIndex): string[] {
+ return find_upstream_atomic(graph, source_id, rev_conn_index,
+ (_node_id: string, node: AtomicFlowBlockData) => {
+ if (node.value.options.block_function === 'op_fork_execution') {
+ return 'capture';
+ }
+ else {
+ return 'continue';
+ }
+ });
+}
+
+function next_empty_fork_output_index(graph: FlowGraph, fork_id: string, conn_index: EdgeIndex): number {
+ const filled_outputs = [];
+ for (const conn of conn_index[fork_id] || []) {
+ filled_outputs[conn.from.output_index] = true;
+ }
+
+ let i = 0;
+ while (filled_outputs[i]) {
+ i++;
+ }
+
+ return i;
+}
+
+function get_first_user_per_ast(graph: FlowGraph, getter: string, conn_index: EdgeIndex, rev_conn_index: EdgeIndex): string[] {
+ const users = find_downstream_atomic(graph, getter, conn_index,
+ (_node_id: string, node: AtomicFlowBlockData) => {
+ if (node.value.options.type !== 'getter') {
+ return 'capture';
+ }
+ else {
+ return 'continue';
+ }
+ });
+
+ // Build candidate index
+ const candidates: {[key: string]: boolean} = {};
+ for (const user of users) {
+ candidates[user] = true;
+ }
+
+ // For each item in list, check if it has another candidate up.
+ // If it has one discard it.
+ for (const user of users) {
+ const scan_controller = ((node_id: string, node: FlowGraphNode) => {
+ if (node.data.type !== AtomicFlowBlock.GetBlockType()) {
+ return 'continue';
+ }
+
+ const block = node.data as AtomicFlowBlockData;
+ if ((node_id !== user) && (candidates[node_id])) {
+ return 'stop'; // This path has another candidate, so it's not usable
+ }
+ else if (block.value.options.type === 'trigger') {
+ return 'capture'; // A path to a trigger found with no other candidates before
+ }
+ else {
+ return 'continue';
+ }
+ });
+
+ if (!scan_pulse_upstream(graph, user, rev_conn_index, scan_controller)) {
+ delete candidates[user];
+ }
+ }
+
+ return Object.keys(candidates);
+}
+
+function get_number_of_uses(graph: FlowGraph, operation: string, getter: string, conn_index: EdgeIndex): number {
+ let count = 0;
+
+ for (const conn of conn_index[getter]) {
+ const scan_controller = ((node_id: string, node: FlowGraphNode) => {
+ if (node.data.type !== AtomicFlowBlock.GetBlockType()) {
+ throw new Error("Unexpected: found input to non-atomic block");
+ }
+
+ const block = node.data as AtomicFlowBlockData;
+
+ if (node_id === operation) {
+ return 'capture';
+ }
+ else if (block.value.options.type !== 'getter') {
+ return 'stop';
+ }
+ else {
+ return 'continue';
+ }
+ });
+ if (scan_downstream(graph, conn.to.id, conn_index, scan_controller)) {
+ count++;
+ }
+ }
+
+ return count;
+}
+
+export function extract_internally_reused_arguments(graph: FlowGraph): FlowGraph {
+ const conn_index = index_connections(graph);
+ const rev_conn_index = reverse_index_connections(graph);
+
+ // Steps
+ //
+ // 1. Find all getter blocks with more than one out connection.
+ // 2. For each AST path find first block in each AST where is called.
+ // 3. If it's used >1 time in that block
+ // 4. Invoke before it to set the value (type: cache-value?)
+
+ const getters = graph_scan_atomic_nodes(graph, (_node_id: string, node: AtomicFlowBlockData) => {
+ return node.value.options.type === 'getter';
+ });
+
+ for (const getter of getters) {
+ const ast_tops = get_first_user_per_ast(graph, getter, conn_index, rev_conn_index);
+ for (const top of ast_tops) {
+ if (get_number_of_uses(graph, top, getter, conn_index) > 1) {
+
+ const ref = uuidv4();
+
+ const [block_options, synth_in, synth_out] = AtomicFlowBlock.add_synth_io(OP_PRELOAD_BLOCK);
+ const preload_op = {
+ type: AtomicFlowBlock.GetBlockType(),
+ value: {
+ options: block_options,
+ synthetic_input_count: synth_in,
+ synthetic_output_count: synth_out,
+ }
+ };
+
+ graph.nodes[ref] = { data: preload_op, position: null };
+
+ rev_conn_index[ref] = [];
+ conn_index[ref] = [];
+
+ // Introduce preload op before the top of AST
+ for (let i = 0; i < rev_conn_index[top].length; i++) {
+ const incoming = rev_conn_index[top][i];
+
+ if (!is_pulse_output(graph.nodes[incoming.from.id], incoming.from.output_index)) {
+ continue;
+ }
+
+ if (incoming.to.input_index !== 0) {
+ throw new Error("Unexpected: Moving block to inject preload. Pulse input on port != 0")
+ }
+
+ for (const conn of conn_index[incoming.from.id]) {
+ if (conn.to.id === top) {
+ conn.to.id = ref;
+ }
+ }
+
+ incoming.to.id = ref;
+ rev_conn_index[ref].push(incoming);
+ rev_conn_index[top].splice(i, 1);
+ i--;
+ }
+
+ // Connect preloader to AST top
+ const conn_to_top = {
+ from: { id: ref, output_index: 0 },
+ to: { id: top, input_index: 0 },
+ index: null as (number | null),
+ };
+
+ graph.edges.push(conn_to_top);
+ conn_to_top.index = graph.edges.length - 1;
+
+ rev_conn_index[top].push(conn_to_top);
+ conn_index[ref].push(conn_to_top);
+
+ // Connect preloader to getter
+ const conn_to_arg = {
+ from: { id: getter, output_index: 0 },
+ to: { id: ref, input_index: 1 },
+ index: null as (number | null),
+ };
+
+ graph.edges.push(conn_to_arg);
+ conn_to_arg.index = graph.edges.length - 1;
+
+ rev_conn_index[ref].push(conn_to_arg);
+ conn_index[getter].push(conn_to_arg);
+ }
+ }
+ }
+
+ return graph;
+}
+
+export function lift_common_ops(graph: FlowGraph): FlowGraph {
+ let updated = false;
+
+ do {
+ let conn_index = index_connections(graph);
+ let rev_conn_index = reverse_index_connections(graph);
+
+ updated = false;
+
+ const forks = find_forks(graph);
+ for (const fork_id of forks) {
+ const next_downstream_joins = find_downstream_joins(graph, fork_id, conn_index);
+ if (next_downstream_joins.length > 1) {
+ const upstream_forks = find_upstream_forks(graph, fork_id, rev_conn_index);
+
+ if (upstream_forks.length === 0) {
+ continue; // Not the right fork level, try with another
+ }
+
+ for (const up_fork_id of upstream_forks) {
+ const paths = get_paths_between(graph, up_fork_id, fork_id, conn_index);
+ if (paths.length === 0) {
+ throw new Error(`AssertionError: Expected path between forks (${up_fork_id} -> ${fork_id})`);
+ }
+
+ if (paths.length > 1) {
+ throw new Error('NOT IMPLEMENTED: Add fork when lifting')
+ }
+
+ for (const fork_input of rev_conn_index[up_fork_id]) {
+ // Connect paths leading to upper fork to the moved path
+ const prev_idx = fork_input.index;
+ graph.edges[prev_idx].to = graph.edges[paths[0][0]].to;
+ }
+
+ let edge_indexes_to_delete = [];
+ for (const fork_output of conn_index[up_fork_id]) {
+ // Connect paths out of the upper fork to the bottom one
+ if (fork_output.index === paths[0][0]) {
+ // Ignore input of the moved path
+ edge_indexes_to_delete.push(fork_output.index);
+ }
+ else {
+ graph.edges[fork_output.index].from = {
+ id: fork_id,
+ output_index: next_empty_fork_output_index(graph, fork_id, conn_index),
+ }
+
+ if (!conn_index[fork_id]) {
+ conn_index[fork_id] = [];
+ }
+
+ // Just keep track of the new connections on the fork, to be able to call `next_empty_fork_output_index`
+ conn_index[fork_id].push(
+ Object.assign({index: fork_output.index}, graph.edges[fork_output.index]) as IndexedFlowGraphEdge
+ );
+ }
+ }
+
+ // Go through the edge-to-remove list in descending order to
+ // avoid keeping track of the index changes
+ for (const edge of edge_indexes_to_delete.sort((a, b) => b - a)) {
+ graph.edges.splice(edge, 1);
+ }
+ delete graph.nodes[up_fork_id];
+
+ // TODO: Might have to recompute conn_index & rev_conn_index
+ // We don't do this now as it's going back to the top of the
+ // loop and they'll get recomputed anyway.
+ }
+ updated = true;
+ break; // Moved things around, better to restart from a cleaner state
+ }
+ }
+ } while (updated);
+ return graph;
+}
+
+function is_streaming_node(conn_index: EdgeIndex, rev_conn_index: EdgeIndex, graph: FlowGraph, nodeId: string): boolean {
+ const node = graph.nodes[nodeId];
+ if (isAtomicFlowBlockData(node.data)) {
+ const nodeHasPulseInputs = (rev_conn_index[nodeId] || []).some( (edge: IndexedFlowGraphEdge) => is_pulse_output( graph.nodes[edge.from.id], edge.from.output_index ) );
+
+ return !nodeHasPulseInputs;
+ }
+ else if (isDirectValueBlockData(node.data) || isEnumDirectValueBlockData(node.data)) {
+ return true;
+ }
+ else if (isUiFlowBlockData(node.data)) {
+ const nodeHasPulseInputs = (rev_conn_index[nodeId] || []).some( (edge: IndexedFlowGraphEdge) => is_pulse_output( graph.nodes[edge.from.id], edge.from.output_index ) );
+
+ return !nodeHasPulseInputs;
+ }
+ else {
+ throw new Error(`Unexpected block type: ${node.data.type}`);
+ }
+}
+
+function is_stepped_node(conn_index: EdgeIndex, rev_conn_index: EdgeIndex, graph: FlowGraph, nodeId: string): boolean {
+ return !is_streaming_node(conn_index, rev_conn_index, graph, nodeId);
+}
+
+export function split_streaming_after_stepped(graph: FlowGraph): FlowGraph {
+ // Detect flow blocks preceded by a stepping one
+ const backTransitions = [];
+
+ {
+ // Scope the indexes, as they'll be unusabe when we start updating the graph.
+ const conn_index = index_connections(graph);
+ const rev_conn_index = reverse_index_connections(graph);
+
+ for (const nodeId of Object.keys(graph.nodes)) {
+ if (is_streaming_node(conn_index, rev_conn_index, graph, nodeId)) {
+ for (const conn of rev_conn_index[nodeId] || []) {
+ if (is_stepped_node(conn_index, rev_conn_index, graph, conn.from.id)) {
+ backTransitions.push(graph.edges[conn.index]);
+ }
+ }
+ }
+ }
+ }
+
+ for (const conn of backTransitions) {
+ // Mark origin block as report-state
+ const origSourceBlock = conn.from.id;
+ const origSourcePort = conn.from.output_index;
+ const source = graph.nodes[origSourceBlock];
+
+ if (isAtomicFlowBlockData(source.data)) {
+ source.data.value.report_state = true;
+ }
+
+ // Update connection to generate from a (virtual) on-block-update
+ const onBlockRunRef = uuidv4();
+
+ const [block_options, synth_in, synth_out] = AtomicFlowBlock.add_synth_io(OP_ON_BLOCK_RUN);
+ const on_run_op = {
+ type: AtomicFlowBlock.GetBlockType(),
+ value: {
+ options: block_options,
+ synthetic_input_count: synth_in,
+ synthetic_output_count: synth_out,
+ }
+ };
+
+ graph.nodes[onBlockRunRef] = { data: on_run_op, position: null };
+
+ conn.from.id = onBlockRunRef;
+ conn.from.output_index = 0;
+
+ // Add direct value to notify the on-block-update which block to monitor
+ const blockRunIdValue = uuidv4();
+
+ graph.nodes[blockRunIdValue] = { data: {
+ type: DirectValue.GetBlockType(),
+ value: {
+ type: 'string',
+ value: origSourceBlock,
+ }
+ }, position: null };
+
+ graph.edges.push({
+ from: {
+ id: blockRunIdValue,
+ output_index: 0,
+ },
+ to: {
+ id: onBlockRunRef,
+ input_index: 0,
+ }
+ });
+
+ // Add direct value for the output port
+ const blockRunPortIdxValue = uuidv4();
+
+ graph.nodes[blockRunPortIdxValue] = { data: {
+ type: DirectValue.GetBlockType(),
+ value: {
+ type: 'integer',
+ value: origSourcePort,
+ }
+ }, position: null };
+
+ graph.edges.push({
+ from: {
+ id: blockRunPortIdxValue,
+ output_index: 0,
+ },
+ to: {
+ id: onBlockRunRef,
+ input_index: 1,
+ }
+ });
+ }
+
+ return graph;
+}
diff --git a/frontend/src/app/flow-editor/graph_utils.ts b/frontend/src/app/flow-editor/graph_utils.ts
new file mode 100644
index 00000000..c4874062
--- /dev/null
+++ b/frontend/src/app/flow-editor/graph_utils.ts
@@ -0,0 +1,39 @@
+import { FlowGraph, FlowGraphEdge } from './flow_graph';
+
+export interface IndexedFlowGraphEdge extends FlowGraphEdge {
+ index: number,
+};
+
+export type EdgeIndex = {[key: string]:IndexedFlowGraphEdge[]};
+
+export function index_connections(graph: FlowGraph): EdgeIndex {
+ const index: {[key: string]: IndexedFlowGraphEdge[]} = {};
+
+ let idx = -1;
+ for (const conn of graph.edges) {
+ idx++;
+
+ if (!index[conn.from.id]) {
+ index[conn.from.id] = [];
+ }
+ index[conn.from.id].push(Object.assign({ index: idx }, conn));
+ }
+
+ return index;
+}
+
+export function reverse_index_connections(graph: FlowGraph): EdgeIndex {
+ const index: {[key: string]: IndexedFlowGraphEdge[]} = {};
+
+ let idx = -1;
+ for (const conn of graph.edges) {
+ idx++;
+
+ if (!index[conn.to.id]) {
+ index[conn.to.id] = [];
+ }
+ index[conn.to.id].push(Object.assign({ index: idx }, conn));
+ }
+
+ return index;
+}
diff --git a/frontend/src/app/flow-editor/graph_validation.ts b/frontend/src/app/flow-editor/graph_validation.ts
new file mode 100644
index 00000000..0a786539
--- /dev/null
+++ b/frontend/src/app/flow-editor/graph_validation.ts
@@ -0,0 +1,342 @@
+import { AtomicFlowBlock, AtomicFlowBlockData } from './atomic_flow_block';
+import { FlowGraph, FlowGraphEdge, FlowGraphNode } from './flow_graph';
+import { get_unreachable } from './graph_analysis';
+import { index_connections, reverse_index_connections, EdgeIndex } from './graph_utils';
+import { find_downstream, is_pulse_output, is_pulse } from './graph_transformations';
+
+const LOOP_FOUND = '__loop_found__';
+
+function get_edges_for_nodes(graph: FlowGraph, nodes: {[key:string]: FlowGraphNode }): FlowGraphEdge[] {
+ const edges: FlowGraphEdge[] = [];
+ for (const conn of graph.edges) {
+ if (nodes[conn.from.id] && nodes[conn.to.id]) {
+ edges.push(conn);
+ }
+ }
+ return edges;
+}
+
+function get_streaming_section(graph: FlowGraph): FlowGraph {
+ const nodes: {[key:string]: FlowGraphNode } = {};
+ for (const block_id of Object.keys(graph.nodes)) {
+ const block = graph.nodes[block_id];
+ if (block.data.type === AtomicFlowBlock.GetBlockType()){
+ const data = block.data as AtomicFlowBlockData;
+
+ const inputs = data.value.options.inputs || [];
+ const outputs = data.value.options.outputs || [];
+
+ // If it has no pulse inputs or outputs its a streaming block
+ if ((inputs.filter(v => is_pulse(v)).length === 0)
+ && (outputs.filter(v => is_pulse(v)).length === 0)) {
+ nodes[block_id] = block;
+ }
+ }
+ else {
+ nodes[block_id] = block;
+ }
+ }
+
+
+ return {
+ nodes: nodes,
+ edges: get_edges_for_nodes(graph, nodes),
+ };
+}
+
+function get_stepped_section(graph: FlowGraph): FlowGraph {
+ const nodes: {[key:string]: FlowGraphNode } = {};
+ for (const block_id of Object.keys(graph.nodes)) {
+ const block = graph.nodes[block_id];
+ if (block.data.type === AtomicFlowBlock.GetBlockType()){
+ const data = block.data as AtomicFlowBlockData;
+
+ const inputs = data.value.options.inputs || [];
+ const outputs = data.value.options.outputs || [];
+
+ // If it has no pulse inputs or outputs its a streaming block
+ if ((inputs.filter(v => is_pulse(v)).length > 0)
+ || (outputs.filter(v => is_pulse(v)).length > 0)) {
+ nodes[block_id] = block;
+ }
+ }
+ }
+
+ return {
+ nodes: nodes,
+ edges: get_edges_for_nodes(graph, nodes),
+ };
+}
+
+function validate_streaming_no_loop_around(_graph: FlowGraph,
+ connections_index: {[key: string]:FlowGraphEdge[]},
+ block_id: string): {[key:string]: boolean} {
+
+ function aux(block_id:string, top: {[key:string]: boolean}): {[key:string]: boolean} {
+ const reached: {[key: string]: boolean} = {};
+ reached[block_id] = true ;
+
+ for (const conn of connections_index[block_id] || []) {
+ if (top[conn.to.id]) {
+ throw new Error(`ValidationError: Loop in streaming section around block (id=${conn.to.id})`)
+ }
+
+ const col_top: {[key: string]: boolean} = {}
+ col_top[conn.to.id] = true;
+ Object.assign(col_top, top);
+
+ const reached_in_col = aux(conn.to.id, col_top);
+ Object.assign(reached, reached_in_col);
+ }
+
+ return reached;
+ }
+
+ const top: {[key: string]: boolean} = {};
+ top[block_id] = true ;
+ const reached = aux(block_id, top);
+
+ return reached;
+}
+
+function validate_no_loops_in_streaming_section(graph: FlowGraph) {
+ const streaming_graph = get_streaming_section(graph);
+
+ const connections_index = index_connections(streaming_graph);
+ const validated: {[key:string]: boolean} = {};
+
+ for (const block_id of Object.keys(streaming_graph.nodes)) {
+ if (!validated[block_id]) {
+ const validated_group = validate_streaming_no_loop_around(streaming_graph, connections_index, block_id)
+ Object.assign(validated, validated_group);
+ }
+ }
+}
+
+function validate_that_all_paths_have_fork(graph: FlowGraph,
+ join_bottom_id: string,
+ conn_index: {[key: string]:FlowGraphEdge[]}) {
+
+ function try_find_upwards_without_fork(bottom_id: string, depth: number, reached: {[key: string]: boolean},
+ acc: { control_if: string[] }): string {
+ const block = graph.nodes[bottom_id];
+ let control_if_acc = acc.control_if;
+
+ if (block.data.type === AtomicFlowBlock.GetBlockType()) {
+ const a_block = block.data as AtomicFlowBlockData;
+ if (a_block.value.options.block_function === "op_fork_execution") {
+ return null; // This path is not problematic
+ }
+ else if (a_block.value.options.block_function === "control_if_else") {
+ if (control_if_acc) {
+ control_if_acc.push(bottom_id);
+ }
+ }
+ else if (a_block.value.options.block_function === "trigger_when_first_completed") {
+ // This disables the problem related to the control_if accumulator
+ control_if_acc = null;
+ }
+ }
+
+ const upwards = conn_index[bottom_id];
+ if (!upwards || !upwards.length) {
+ return bottom_id; // Found a problematic path
+ }
+
+ reached[bottom_id] = true;
+
+ let conn_out_of_loop = 0;
+ for (const conn of upwards) {
+ if (reached[conn.from.id]) {
+ continue; // Skip if already reached
+ }
+
+ const col_reached = Object.assign({}, reached);
+ try {
+ const source = try_find_upwards_without_fork(conn.from.id, depth + 1, col_reached, { control_if: control_if_acc });
+ if (source) {
+ return source;
+ }
+ conn_out_of_loop += 1;
+ }
+ catch (err) {
+ // This is horribly inefficient, as the exception will be thrown
+ // and catched >3000 times.
+ //
+ // As is now, this should only happen when a infinite loop is
+ // found during the validation of the graph. This would signal a
+ // error on the way to work of the function. In that case we can
+ // pay a performance price in exchange of getting as much
+ // information as possible. This should NEVER happen on real
+ // usage of the code, and at most when writing new tests for
+ // problematic data.
+ if (err.message === 'Maximum call stack size exceeded') {
+ err.message = `[Depth ${depth}] ${err.message}. Maybe there's an unmanaged loop?`;
+ }
+ throw err;
+ }
+ }
+
+ if (conn_out_of_loop === 0) {
+ return bottom_id; // Cannot get to a FORK even if all paths are explored
+ }
+ return null;
+ }
+
+ const known_if_blocks: {[key: string]: boolean} = {};
+
+ for (const conn of conn_index[join_bottom_id] ) {
+ const reached: {[key: string]: boolean} = {};
+ reached[conn.to.id] = true;
+
+ const acc = { control_if: [] as string[] };
+ const source = try_find_upwards_without_fork(conn.from.id, 1, reached, acc);
+ if (source) {
+ throw new Error(`ValidationError: Block (id:${source}) can get to Join (id:${join_bottom_id}) with no fork.`
+ + ' Joins can only be done between flows that have previously forked.');
+ }
+
+ // Check that no two connections lead to the same IF
+ for (const block of acc.control_if) {
+ if (known_if_blocks[block]) {
+ throw new Error(`ValidationError: A single conditional block (id:${block}) has two connections to a fork join block.`
+ + ' From an IF block only one connection can be established to the Join block.'
+ + ' Consider merging the conditional paths using a `trigger_when_first_completed` block.');
+ }
+ known_if_blocks[block] = true;
+ }
+ }
+}
+
+function validate_joins_only_after_forks(graph: FlowGraph) {
+ const stepped_graph = get_stepped_section(graph);
+ const connections_index = reverse_index_connections(stepped_graph);
+ for (const block_id of Object.keys(stepped_graph.nodes)) {
+ const block = stepped_graph.nodes[block_id];
+
+ if (block.data.type === AtomicFlowBlock.GetBlockType()) {
+ const a_block = block.data as AtomicFlowBlockData;
+ if (a_block.value.options.block_function === "trigger_when_all_completed") {
+ validate_that_all_paths_have_fork(stepped_graph, block_id, connections_index);
+ }
+ }
+ }
+}
+
+function validate_no_loops_around_block(graph: FlowGraph, block_id: string, conn_index: EdgeIndex) {
+ // Look for the block_id starting from each connection
+ for (const conn of conn_index[block_id]) {
+ find_downstream(graph, conn.to.id, conn_index, (node_id: string, _node: FlowGraphNode) => {
+ if (node_id === block_id) {
+ throw LOOP_FOUND;
+ }
+
+ return 'continue';
+ });
+ }
+}
+
+function validate_jumps_not_out_of_forks(graph: FlowGraph) {
+ const stepped_graph = get_stepped_section(graph);
+ const connections_index = index_connections(stepped_graph);
+ for (const block_id of Object.keys(stepped_graph.nodes)) {
+ const block = stepped_graph.nodes[block_id];
+
+ if (block.data.type === AtomicFlowBlock.GetBlockType()) {
+ const a_block = block.data as AtomicFlowBlockData;
+ if (a_block.value.options.block_function === "op_fork_execution") {
+ try {
+ validate_no_loops_around_block(stepped_graph, block_id, connections_index);
+ }
+ catch (err) {
+ if (err === LOOP_FOUND) {
+ throw new Error('ValidationError: Loop around Fork blocks not allowed.'
+ + ` Found around block (id:${block_id})`)
+ }
+ else {
+ throw err;
+ }
+ }
+ }
+ }
+ }
+}
+
+function validate_no_blocks_with_disconnected_required_inputs(graph: FlowGraph) {
+ const rev_conn = reverse_index_connections(graph);
+ for (const block_id of Object.keys(graph.nodes)) {
+ const block = graph.nodes[block_id];
+ if (block.data.type === AtomicFlowBlock.GetBlockType()) {
+ const a_block = block.data as AtomicFlowBlockData;
+
+ const block_cons = rev_conn[block_id];
+
+ if (a_block.value.options.inputs) {
+ for (let input_index = 0; input_index < a_block.value.options.inputs.length; input_index++) {
+ const input = a_block.value.options.inputs[input_index];
+ if (input.required) {
+ const connections_to_input = block_cons.filter(
+ (v: FlowGraphEdge) => v.to.input_index === input_index
+ );
+ if (connections_to_input.length === 0) {
+ throw new Error(`ValidationError: Required input has no connections (block:${block_id},input:${input_index})`);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+function validate_wait_for_value_blocks(graph: FlowGraph) {
+ const rev_conn = reverse_index_connections(graph);
+ for (const block_id of Object.keys(graph.nodes)) {
+ const block = graph.nodes[block_id];
+ if (block.data.type === AtomicFlowBlock.GetBlockType()) {
+ const a_block = block.data as AtomicFlowBlockData;
+ if (a_block.value.options.block_function !== 'control_wait_for_next_value') {
+ continue;
+ }
+
+ const block_cons = rev_conn[block_id];
+
+ // Only allow pulse connections, from variables or bridge signals
+ for (const conn of block_cons) {
+ if (is_pulse_output(graph.nodes[conn.from.id], conn.from.output_index)) {
+ continue;
+ }
+
+ const value_input = graph.nodes[conn.from.id];
+ if (value_input.data.type !== AtomicFlowBlock.GetBlockType()) {
+ throw new Error(`ValidationError: Wait for value does not accept a constant as input (block:${block_id})`);
+ }
+
+ const inp_block = value_input.data as AtomicFlowBlockData;
+ const func = inp_block.value.options.block_function;
+ if ((func !== 'data_variable') // Check changes in variable
+ && (func !== 'flow_utc_time') && (func !== 'flow_utc_date') // Check time trigger
+ && (!func.startsWith('services.')) // Check bridge trigger/getter
+ ) {
+ throw new Error(`ValidationError: Wait for value does not accept a getter as input (block:${block_id},func:${func})`);
+ }
+ }
+
+ }
+ }
+}
+
+export function validate(graph: FlowGraph) {
+ // Reject loops in streaming section
+ validate_no_loops_in_streaming_section(graph);
+ const unreachable = get_unreachable(graph);
+ if (unreachable.length > 0) {
+ throw new Error(`ValidationError: Unreachable blocks (${unreachable})`);
+ }
+ validate_joins_only_after_forks(graph);
+ validate_jumps_not_out_of_forks(graph);
+ validate_no_blocks_with_disconnected_required_inputs(graph);
+
+ validate_wait_for_value_blocks(graph);
+
+ return true;
+}
diff --git a/frontend/src/app/flow-editor/platform_facilities.ts b/frontend/src/app/flow-editor/platform_facilities.ts
new file mode 100644
index 00000000..96e9df60
--- /dev/null
+++ b/frontend/src/app/flow-editor/platform_facilities.ts
@@ -0,0 +1 @@
+export const TIME_MONITOR_ID = "0093325b-373f-4f1c-bace-4532cce79df4";
diff --git a/frontend/src/app/flow-editor/toolbox-flow-button.ts b/frontend/src/app/flow-editor/toolbox-flow-button.ts
new file mode 100644
index 00000000..cf7fadd2
--- /dev/null
+++ b/frontend/src/app/flow-editor/toolbox-flow-button.ts
@@ -0,0 +1,30 @@
+import { FlowActuator } from './flow_block';
+
+export type ToolboxFlowButtonInitOps = {
+ message: string,
+ action: () => void,
+};
+
+export class ToolboxFlowButton implements FlowActuator {
+ message: string;
+ action: () => void;
+
+ constructor(ops: ToolboxFlowButtonInitOps) {
+ this.message = ops.message;
+ this.action = ops.action;
+ }
+
+ render(div: HTMLDivElement): HTMLElement {
+ const element = document.createElement('button');
+ element.classList.add('toolbox-flow-button')
+ element.innerText = this.message;
+
+ div.appendChild(element);
+ return element;
+ }
+
+ onclick(): void {
+ return this.action();
+ }
+
+}
diff --git a/frontend/src/app/flow-editor/toolbox.ts b/frontend/src/app/flow-editor/toolbox.ts
new file mode 100644
index 00000000..7b1784ca
--- /dev/null
+++ b/frontend/src/app/flow-editor/toolbox.ts
@@ -0,0 +1,279 @@
+import { BlockExhibitor, BlockGenerator } from './block_exhibitor';
+import { BlockManager } from './block_manager';
+import { FlowBlock, Position2D, FlowBlockOptions, FlowActuator } from './flow_block';
+import { FlowWorkspace } from './flow_workspace';
+import { UiSignalService } from '../services/ui-signal.service';
+import { Session } from '../session';
+import { ADVANCED_CATEGORY, INTERNAL_CATEGORY } from './base_toolbox_description';
+import { uuidv4 } from './utils';
+
+export type ActuatorGenerator = () => FlowActuator;
+
+export class Toolbox {
+ toolboxDiv: HTMLDivElement;
+ hideButtonDiv: HTMLDivElement;
+ blockShowcase: HTMLDivElement;
+ categories: { [key: string]: { div: HTMLDivElement, content: HTMLDivElement } } = {};
+ categoryShortcuts: { [key: string]: HTMLLIElement } = {};
+ blocks: FlowBlockOptions[] = [];
+ categoryShortcutList: HTMLDivElement;
+ categoryShortcutListContents: HTMLUListElement;
+
+ public static BuildOn(baseElement: HTMLElement,
+ workspace: FlowWorkspace,
+ uiSignalService: UiSignalService,
+ session: Session,
+ no_dom: boolean,
+ behavior: { portrait: boolean, autohide: boolean },
+ ): Toolbox {
+ let toolbox: Toolbox;
+ try {
+ toolbox = new Toolbox(baseElement, workspace, uiSignalService, session, no_dom, behavior);
+ toolbox.init();
+ }
+ catch(err) {
+ toolbox.dispose();
+
+ throw err;
+ }
+
+ return toolbox;
+ }
+
+
+ private constructor(private baseElement: HTMLElement,
+ private workspace: FlowWorkspace,
+ public uiSignalService: UiSignalService,
+ private session: Session,
+ private no_dom: boolean,
+ private behavior: { portrait: boolean, autohide: boolean },
+ ) { }
+
+ onResize() {}
+
+ dispose() {
+ this.baseElement.removeChild(this.toolboxDiv);
+ }
+
+ init() {
+ if (this.no_dom) {
+ return;
+ }
+
+ // Toolbox
+ this.toolboxDiv = document.createElement('div');
+ const classes = this.toolboxDiv.classList;
+ classes.add('toolbox');
+ if (this.behavior.portrait) {
+ classes.add('portrait')
+ }
+ else {
+ classes.add('landscape');
+ }
+ if (this.behavior.autohide) {
+ classes.add('collapsed');
+ }
+
+ // Hide button
+ this.hideButtonDiv = document.createElement('div');
+ const button = document.createElement('button');
+ button.onclick = () => {
+ classes.add('collapsed');
+ }
+ button.innerText = '⌄';
+ this.hideButtonDiv.setAttribute('class', 'hide-button-section');
+ this.hideButtonDiv.appendChild(button);
+ this.toolboxDiv.appendChild(this.hideButtonDiv);
+
+ this.baseElement.appendChild(this.toolboxDiv);
+
+ this.categoryShortcutList = document.createElement('div');
+ this.categoryShortcutList.setAttribute('class', 'category-shortcut-list');
+
+ this.categoryShortcutListContents = document.createElement('ul');
+ this.categoryShortcutListContents.setAttribute('class', 'contents');
+ this.categoryShortcutList.appendChild(this.categoryShortcutListContents);
+ this.toolboxDiv.appendChild(this.categoryShortcutList);
+
+ this.blockShowcase = document.createElement('div');
+ this.blockShowcase.setAttribute('class', 'showcase');
+ this.toolboxDiv.appendChild(this.blockShowcase);
+ }
+
+ setCategory(cat:{ id: string, name: string }) {
+ const [div, updated] = this.getOrCreateCategory(cat);
+ if (!updated && !this.no_dom) {
+ const title = div.getElementsByClassName('category_title')[0] as HTMLDivElement;
+ title.innerText = cat.name;
+ }
+ }
+
+ private getOrCreateCategory(cat:{ id: string, name: string }): [HTMLDivElement, HTMLDivElement, boolean, HTMLLIElement] {
+ let category = this.categories[cat.id];
+ let categoryShortcut = this.categoryShortcuts[cat.id];
+ let created_now = false;
+ let category_div: HTMLDivElement;
+ let category_content: HTMLDivElement;
+
+ if (!category && !this.no_dom) {
+ // Category
+ category_div = document.createElement('div');
+ category_div.setAttribute('class', 'category empty cat_name_' + cat.name + ' cat_id_' + cat.id);
+ this.blockShowcase.appendChild(category_div);
+
+ // Contents
+ category_content = document.createElement('div');
+ category_content.setAttribute('class', 'content');
+ category_div.appendChild(category_content);
+
+ // Title
+ const cat_title = document.createElement('div');
+ cat_title.setAttribute('class', 'category_title');
+ cat_title.innerText = cat.name;
+ category_content.appendChild(cat_title)
+
+ this.categories[cat.id] = {div: category_div, content: category_content};
+
+ created_now = true;
+
+ categoryShortcut = this.categoryShortcuts[cat.id] = document.createElement('li');
+ categoryShortcut.setAttribute('class', 'empty');
+
+ const catName = document.createElement('div');
+ catName.setAttribute('class', 'category-name');
+ catName.innerText = cat.name;
+ categoryShortcut.appendChild(catName);
+
+ categoryShortcut.onclick = () => {
+ // Expand if it's collapsed
+ if (this.toolboxDiv.classList.contains('collapsed')) {
+ this.toolboxDiv.classList.remove('collapsed');
+ }
+
+ // Then scroll to it
+ category_div.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest", });
+ };
+ this.categoryShortcutListContents.appendChild(categoryShortcut);
+ }
+ else if (!this.no_dom) {
+ category_div = category.div;
+ category_content = category.content;
+ }
+
+ return [category_div, category_content, created_now, categoryShortcut];
+ }
+
+ addBlockGenerator(generator: BlockGenerator, category_id: string) {
+ if (this.no_dom) {
+ return;
+ }
+
+ if (category_id === ADVANCED_CATEGORY) {
+ if (!this.session.tags.is_advanced) {
+ return; // Skip advaced blocks if the user has not activated them
+ }
+ }
+
+ if (category_id === INTERNAL_CATEGORY) {
+ return; // Don't show internal blocks
+ }
+
+ const [category_div, category_content, _created_now, category_shortcut] = this.getOrCreateCategory({ id: category_id, name: category_id })
+ category_div.classList.remove('empty');
+ category_content.classList.remove('empty');
+ category_shortcut.classList.remove('empty');
+
+ const block_exhibitor = BlockExhibitor.FromGenerator(generator, category_content);
+ const element = block_exhibitor.getElement();
+ element.onmousedown = element.ontouchstart = (ev: MouseEvent | TouchEvent) => {
+ try {
+ const rect = block_exhibitor.getInnerElementRect();
+
+ if (!rect) {
+ // Hidden block, ignore
+ return;
+ }
+
+ const block = generator(this.workspace, uuidv4());
+ element.classList.add('hidden');
+ this.toolboxDiv.classList.add('subsumed');
+
+ const pos = this.workspace._getPositionFromEvent(ev);
+ // Center rect on cursor
+ rect.x = pos.x - (rect.width / this.workspace.getInvZoomLevel()) / 2;
+ rect.y = pos.y - (rect.height / this.workspace.getInvZoomLevel()) / 2;
+
+ const block_id = this.workspace.drawAbsolute(block, rect);
+
+ (this.workspace as any)._mouseDownOnBlock(pos, block, (ev: Position2D) => {
+
+ element.classList.remove('hidden');
+ this.toolboxDiv.classList.remove('subsumed');
+
+ // Check if the block was dropped on the toolbox, if so remove it
+ const toolboxRect = this.toolboxDiv.getBoundingClientRect();
+ if ((ev.x >= toolboxRect.x) && (ev.x <= toolboxRect.x + toolboxRect.width)) {
+ if ((ev.y >= toolboxRect.y) && (ev.y <= toolboxRect.y + toolboxRect.height)) {
+ // Dropped on toolbox
+ console.log("Dropped on toolbox, cleaning up");
+ this.workspace.removeBlock(block_id);
+ }
+ }
+
+ });
+
+ if ((ev as TouchEvent).targetTouches) {
+ // Redirect touch events to the canvas. If we don't do this,
+ // the canvas won't receive touchmove or touchend events.
+ ev.preventDefault();
+
+ element.ontouchmove = (ev) => {
+ ev.preventDefault();
+ this.workspace.getCanvas().dispatchEvent(new TouchEvent('touchmove', {
+ targetTouches: Array.from(ev.targetTouches)
+ }));
+ }
+ element.ontouchend = (ev) => {
+ element.ontouchmove = null;
+ element.ontouchend = null;
+
+ ev.preventDefault();
+ this.workspace.getCanvas().dispatchEvent(new TouchEvent('touchend', {
+ targetTouches: Array.from(ev.targetTouches)
+ }));
+ }
+ }
+ }
+ catch (err) {
+ console.error(err);
+ }
+ };
+ }
+
+ addActuator(generator: ActuatorGenerator, category_id: string) {
+ if (this.no_dom) {
+ return;
+ }
+
+ if (category_id === ADVANCED_CATEGORY) {
+ if (!this.session.tags.is_advanced) {
+ return; // Skip advaced blocks if the user has not activated them
+ }
+ }
+
+ const [category_div, category_content, _created_now, category_shortcut] = this.getOrCreateCategory({ id: category_id, name: category_id })
+ category_div.classList.remove('empty');
+ category_content.classList.remove('empty');
+ category_shortcut.classList.remove('empty');
+
+ const actuator = generator();
+ const element = actuator.render(category_content);
+ element.onclick = (ev: MouseEvent) => {
+ actuator.onclick();
+ };
+ }
+
+ addBlock(block: FlowBlockOptions) {
+ this.blocks.push(block);
+ }
+}
diff --git a/frontend/src/app/flow-editor/toolbox_builder.ts b/frontend/src/app/flow-editor/toolbox_builder.ts
new file mode 100644
index 00000000..d31a9957
--- /dev/null
+++ b/frontend/src/app/flow-editor/toolbox_builder.ts
@@ -0,0 +1,364 @@
+import { MatDialog } from '@angular/material/dialog';
+import { BridgeConnection } from '../connection';
+import { ConnectionService } from '../connection.service';
+import { AddConnectionDialogComponent } from '../connections/add-connection-dialog.component';
+import { EnvironmentService } from '../environment.service';
+import { ServiceService } from '../service.service';
+import { UiSignalService } from '../services/ui-signal.service';
+import { Session } from '../session';
+import { ResolvedBlockArgument, ResolvedCustomBlock, ResolvedDynamicBlockArgument, ResolvedDynamicSequenceBlockArgument } from '../custom_block';
+import { CustomBlockService } from '../custom_block.service';
+import { iconDataToUrl } from '../utils';
+import { AtomicFlowBlock, isAtomicFlowBlockOptions, AtomicFlowBlockOptions } from './atomic_flow_block';
+import { BaseToolboxDescription } from './base_toolbox_description';
+import { InputPortDefinition, MessageType, OutputPortDefinition } from './flow_block';
+import { FlowWorkspace } from './flow_workspace';
+import { Toolbox } from './toolbox';
+import { ToolboxFlowButton } from './toolbox-flow-button';
+import { ContainerFlowBlock, isContainerFlowBlockOptions } from './ui-blocks/container_flow_block';
+import { isUiFlowBlockOptions, UiFlowBlock } from './ui-blocks/ui_flow_block';
+import { UiToolboxDescription } from './ui-blocks/ui_toolbox_description';
+import { BlockManager } from './block_manager';
+
+
+export function buildBaseToolbox(baseElement: HTMLElement,
+ workspace: FlowWorkspace,
+ uiSignalService: UiSignalService,
+ session: Session,
+ hidden: boolean,
+ behavior: { portrait: boolean, autohide: boolean },
+ ): Toolbox {
+ const tb = Toolbox.BuildOn(baseElement, workspace, uiSignalService, session, hidden, behavior);
+
+ for (const category of [...UiToolboxDescription, ...BaseToolboxDescription]) {
+ tb.setCategory({ id: category.id, name: category.name });
+ for (const block of category.blocks) {
+ tb.addBlock(block);
+
+ if (block.is_internal) {
+ continue; // Skip
+ }
+
+ tb.addBlockGenerator((manager: BlockManager, blockId: string) => {
+
+ const desc = Object.assign({
+ on_io_selected: manager.onIoSelected.bind(manager),
+ on_dropdown_extended: manager.onDropdownExtended.bind(manager),
+ on_inputs_changed: manager.onInputsChanged.bind(manager),
+ }, block);
+
+ if (isAtomicFlowBlockOptions(block)) {
+ return new AtomicFlowBlock(desc as AtomicFlowBlockOptions, blockId);
+ }
+
+ if (isContainerFlowBlockOptions(desc)) {
+ return new ContainerFlowBlock(desc, blockId, uiSignalService);
+ }
+ // This is a more generic class. It has to be checked after
+ // the more specific ones so they have a chance of matching.
+ else if (isUiFlowBlockOptions(desc)) {
+ return new UiFlowBlock(desc, blockId, uiSignalService);
+ }
+ else {
+ throw new Error("Unknown block options: " + JSON.stringify(block))
+ }
+ }, category.id);
+ }
+ }
+
+ return tb;
+}
+
+export async function fromCustomBlockService(baseElement: HTMLElement,
+ workspace: FlowWorkspace,
+ customBlockService: CustomBlockService,
+ serviceService: ServiceService,
+ environmentService: EnvironmentService,
+ programId: string,
+ uiSignalService: UiSignalService,
+ connectionService: ConnectionService,
+ session: Session,
+ dialog: MatDialog,
+ triggerToolboxReload: () => void,
+ hidden: boolean,
+ behavior: { portrait: boolean, autohide: boolean },
+ ): Promise {
+ const base = buildBaseToolbox(baseElement, workspace, uiSignalService, session, hidden, behavior);
+
+ if (hidden) {
+ return base;
+ }
+
+ const availableConnectionsQuery = connectionService.getAvailableBridgesForNewConnectionOnProgram(programId);
+
+ const [connections, services] = await Promise.all([
+ connectionService.getConnectionsOnProgram(programId),
+ serviceService.getAvailableServicesOnProgram(programId),
+ ]);
+
+ const connection_by_id: {[key: string]: BridgeConnection} = {};
+
+ for (const connection of connections) {
+ connection_by_id[connection.bridge_id] = connection;
+ }
+
+ for (const service of services) {
+ base.setCategory({ id: service.id, name: service.name });
+ }
+
+ const skip_resolve_argument_options = true; // Enum options will be filled when needed
+ const blocks = await customBlockService.getCustomBlocksOnProgram(programId, skip_resolve_argument_options);
+ for (const block of blocks) {
+ let icon: string | null = null;
+
+ if (block.show_in_toolbox === false) {
+ continue; // Skip
+ }
+
+ const connection = connection_by_id[block.service_port_id];
+ if (connection) {
+ icon = iconDataToUrl(environmentService, connection.icon, connection.bridge_id);
+ }
+ else {
+ console.error("No connection found for", block, connection_by_id);
+ }
+
+
+ const [message, translationTable] = get_block_message(block);
+
+ let subkey: null | { type: 'argument', index: number } = null;
+ if (block.subkey) {
+ subkey = {
+ type: 'argument',
+ index: translationTable[block.subkey.index + 1],
+ };
+ }
+
+ base.addBlockGenerator((manager: BlockManager, blockId: string) => {
+ return new AtomicFlowBlock({
+ icon: icon,
+ message: message,
+ block_function: 'services.' + block.service_port_id + '.' + block.function_name,
+ type: (block.block_type as any),
+ inputs: get_block_inputs(block),
+ outputs: get_block_outputs(block),
+ key: block.key,
+ subkey: subkey,
+ on_io_selected: manager.onIoSelected.bind(manager),
+ on_dropdown_extended: manager.onDropdownExtended.bind(manager),
+ on_inputs_changed: manager.onInputsChanged.bind(manager),
+ }, blockId);
+ }, block.service_port_id);
+ }
+
+ const availableBridges = await availableConnectionsQuery;
+ for (const bridge of availableBridges) {
+ base.addActuator(() =>
+ new ToolboxFlowButton({
+ message: "Connect to " + bridge.name,
+ action: () => {
+ const dialogRef = dialog.open(AddConnectionDialogComponent, {
+ disableClose: false,
+ data: {
+ programId: programId,
+ bridgeInfo: bridge,
+ }
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!result) {
+ console.log("Cancelled");
+ return;
+ }
+
+ console.debug("Reloading toolbox...");
+ triggerToolboxReload();
+ });
+ }
+ }), bridge.id);
+ }
+
+ return base;
+}
+
+function get_output_indexes(block: ResolvedCustomBlock): number[] {
+ let output_indexes = [];
+ if (block.save_to) {
+ if ((block.save_to as any) === 'undefined') {
+ console.warn('Serialization error on block.save_to');
+ }
+ else if (((block.save_to as any).type !== 'argument')
+ || !(((block.save_to as any).index) || ((block.save_to as any).index === 0))) {
+
+ console.error('BLOCK save to', block);
+ }
+ else {
+ output_indexes.push((block.save_to as any).index);
+ }
+ }
+
+ return output_indexes;
+}
+
+export function get_block_message(block: ResolvedCustomBlock): [string, number[]] {
+ const output_indexes = get_output_indexes(block);
+
+ const translationTable: number[] = [];
+ let offset = 0;
+
+ const message = block.message.replace(/%(\d+)/g, (_match, digits) => {
+ const num = parseInt(digits);
+ if (output_indexes.indexOf(num - 1) < 0) { // %num are 1-indexed
+ translationTable[num] = num - offset;
+ return `%i${num - offset}`;
+ }
+ else {
+ offset += 1;
+ if (output_indexes.length !== 1) {
+ console.error('TODO: Index output remapping', block);
+ }
+ return '%o1';
+ }
+ });
+
+ return [message, translationTable];
+}
+
+export function get_block_inputs(block: ResolvedCustomBlock): InputPortDefinition[] {
+ // Remove save_to
+ const skipped_indexes = get_output_indexes(block);
+
+ return (block.arguments
+ .filter((_value, index) => skipped_indexes.indexOf(index) < 0)
+ .map((value) => (get_block_arg(block, value)) ));
+}
+
+export function get_block_arg(block: ResolvedCustomBlock, arg: ResolvedBlockArgument): InputPortDefinition {
+ if ((arg as ResolvedDynamicSequenceBlockArgument).callback_sequence) {
+ const dyn_arg = (arg as ResolvedDynamicSequenceBlockArgument);
+
+ return {
+ type: 'enum_sequence',
+ enum_sequence: dyn_arg.callback_sequence,
+ enum_namespace: block.service_port_id,
+ }
+ }
+ else if ((arg as ResolvedDynamicBlockArgument).callback) {
+ const dyn_arg = (arg as ResolvedDynamicBlockArgument);
+
+ return {
+ type: 'enum',
+ enum_name: dyn_arg.callback,
+ enum_namespace: block.service_port_id,
+ }
+ }
+ else {
+ return {
+ type: get_arg_type(arg),
+ };
+ }
+}
+
+export function get_block_outputs(block: ResolvedCustomBlock): OutputPortDefinition[] {
+ if (block.block_type === 'getter') {
+ let result_type: MessageType = 'any';
+
+ switch (block.block_result_type) {
+ case 'string':
+ case 'boolean':
+ case 'integer':
+ case 'float':
+ result_type = block.block_result_type;
+ break
+
+ case 'number':
+ result_type = 'float';
+ break;
+
+ case 'any':
+ case 'struct':
+ result_type = 'any';
+ break;
+
+ case 'list':
+ result_type = 'list';
+ break;
+
+ case null:
+ console.warn('Return type not set on', block);
+ break;
+
+ default:
+ console.error("Unknown type", block.block_result_type);
+ }
+
+ return [{
+ type: result_type,
+ }];
+ }
+
+
+ // Derive from save_to
+ if (!block.save_to) {
+ return [];
+ }
+ if ((block.save_to as any) === 'undefined') {
+ console.warn('Serialization error on block.save_to');
+ return [];
+ }
+
+ if (((block.save_to as any).type !== 'argument')
+ || !(((block.save_to as any).index) || ((block.save_to as any).index === 0))) {
+
+ console.error('BLOCK save to', block);
+ }
+
+ const arg = block.arguments[(block.save_to as any).index];
+ if (!arg) {
+ console.error('BLOCK save to', block);
+ return [];
+ }
+
+ return [{
+ type: get_arg_type(arg),
+ }];
+}
+
+export function get_arg_type(arg: any): MessageType {
+ let argType = arg.type;
+
+ if (arg.type === 'variable') {
+ if (!arg.var_type) {
+ return 'any';
+ }
+ argType = arg.var_type;
+ }
+
+ let result_type = 'any';
+ switch (argType) {
+ case 'string':
+ case 'boolean':
+ case 'integer':
+ case 'float':
+ result_type = argType;
+ break
+
+ case 'number':
+ result_type = 'float';
+ break;
+
+ case 'any':
+ case 'struct':
+ result_type = 'any';
+ break;
+
+ case null:
+ console.warn('Return type not set on', arg);
+ break;
+
+ default:
+ console.error("Unknown type", arg.type);
+ }
+
+ return result_type as MessageType;
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/cannot_set_as_contents_error.ts b/frontend/src/app/flow-editor/ui-blocks/cannot_set_as_contents_error.ts
new file mode 100644
index 00000000..e402d542
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/cannot_set_as_contents_error.ts
@@ -0,0 +1,13 @@
+import { FlowBlock } from "../flow_block";
+
+export class CannotSetAsContentsError extends Error {
+ public readonly problematicContents: FlowBlock[];
+
+ constructor(message: string, problematicContents: FlowBlock[]) {
+ super(message);
+
+ this.name = "CannotSetAsContentsError";
+ this.message = message;
+ this.problematicContents = problematicContents
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/container_flow_block.ts b/frontend/src/app/flow-editor/ui-blocks/container_flow_block.ts
new file mode 100644
index 00000000..1cfe0181
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/container_flow_block.ts
@@ -0,0 +1,220 @@
+import { UiSignalService } from '../../services/ui-signal.service';
+import { BlockManager } from '../block_manager';
+import { Area2D, ContainerBlock, FlowBlock, FlowBlockData, FlowBlockOptions, Movement2D, Resizeable, Position2D } from '../flow_block';
+import { FlowWorkspace } from '../flow_workspace';
+import { Toolbox } from '../toolbox';
+import { CutTree, UiElementWidgetType } from './renderers/ui_tree_repr';
+import { isUiFlowBlockData, UiFlowBlock, UiFlowBlockBuilderInitOps, UiFlowBlockData, UiFlowBlockHandler, UiFlowBlockOptions } from './ui_flow_block';
+
+export type ContainerFlowBlockType = 'container_flow_block';
+export const BLOCK_TYPE = 'container_flow_block';
+
+const PUSH_DOWN_MARGIN = 10;
+const PUSH_RIGHT_MARGIN = 10;
+
+export interface ContainerFlowBlockBuilderInitOps {
+ workspace?: FlowWorkspace,
+}
+
+export type GenTreeProc = (handler: UiFlowBlockHandler, blocks: FlowBlock[]) => CutTree;
+
+export type ContainerFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: ContainerFlowBlock,
+ service: UiSignalService,
+ ops: UiFlowBlockBuilderInitOps) => ContainerFlowBlockHandler;
+
+export interface ContainerFlowBlockHandler extends UiFlowBlockHandler, Resizeable {
+ repositionContents(): void;
+ dropOnEndMove(): Movement2D;
+ getBodyElement(): SVGGraphicsElement;
+ updateContainer(container: UiFlowBlock): void;
+ onContentUpdate: (contents: FlowBlock[]) => void;
+
+ onGetFocus(): void;
+ onLoseFocus(): void;
+
+ readonly container: ContainerFlowBlock;
+ update(): void; // Notify the handler of a change on the properties of the block
+}
+
+export interface ContainerFlowBlockOptions extends UiFlowBlockOptions {
+ builder: ContainerFlowBlockBuilder,
+ subtype: ContainerFlowBlockType,
+ icon?: string,
+ id: UiElementWidgetType,
+ block_id?: string,
+ isPage: boolean,
+
+ gen_tree: GenTreeProc,
+}
+
+
+export interface ContainerFlowBlockData extends UiFlowBlockData {
+ subtype: ContainerFlowBlockType,
+}
+
+export function isContainerFlowBlockOptions(opt: FlowBlockOptions): opt is ContainerFlowBlockOptions {
+ return ((opt as ContainerFlowBlockOptions).subtype === BLOCK_TYPE);
+}
+
+export function isContainerFlowBlockData(data: FlowBlockData): data is ContainerFlowBlockData {
+ return isUiFlowBlockData(data) && ((data as ContainerFlowBlockData).subtype === BLOCK_TYPE);
+}
+
+export class ContainerFlowBlock extends UiFlowBlock implements ContainerBlock {
+ contents: FlowBlock[] = [];
+ options: ContainerFlowBlockOptions;
+ handler: ContainerFlowBlockHandler;
+
+ constructor(options: ContainerFlowBlockOptions,
+ blockId: string,
+ uiSignalService: UiSignalService,
+ ) {
+ super(options, blockId, uiSignalService);
+ }
+
+ addContentBlock(block: FlowBlock): void {
+ if (block === this) {
+ throw Error("Block cannot be it's own content");
+ }
+
+ if (block instanceof UiFlowBlock && (block.hasAncestor(this))) {
+ throw Error("This would create a container ↻ content loop");
+ }
+ else if (block instanceof ContainerFlowBlock && (block.contents.indexOf(this) >= 0)) {
+ throw Error("This would create a container ↻ content loop");
+ }
+
+ this.handler.onContentUpdate(this.contents.concat([block]));
+ this.contents.push(block);
+ }
+
+ removeContentBlock(block: FlowBlock): void {
+ const pos = this.contents.findIndex(b => b === block);
+ if (pos < 0) {
+ console.error(`Block not found on container`);
+ return;
+ }
+
+ this.contents.splice(pos, 1);
+ this.handler.onContentUpdate(this.contents);
+ }
+
+ update(): void {
+ this.handler.onContentUpdate(this.contents);
+ }
+
+ get isPage(): boolean {
+ return !!this.options.isPage;
+ }
+
+ get cannotBeMoved(): boolean {
+ // Right now only the pages cannot be moved, but this might change in the future
+ return this.isPage;
+ }
+
+ getPageTitle(): string {
+ if (!this.isPage) {
+ return null;
+ }
+
+ if (!this.handler.isTextReadable()) {
+ return null;
+ }
+
+ return this.handler.text;
+ }
+
+ public renderAsUiElement(): CutTree {
+ return this.options.gen_tree(this.handler, this.contents.concat([]));
+ }
+
+ public static Deserialize(data: ContainerFlowBlockData, blockId: string, manager: BlockManager, toolbox: Toolbox): FlowBlock {
+ if (data.subtype !== BLOCK_TYPE){
+ throw new Error(`Block subtype mismatch, expected ${BLOCK_TYPE} found: ${data.subtype}`);
+ }
+
+ const options: ContainerFlowBlockOptions = JSON.parse(JSON.stringify(data.value.options));
+ options.on_dropdown_extended = manager.onDropdownExtended.bind(manager);
+ options.on_inputs_changed = manager.onInputsChanged.bind(manager);
+ options.on_io_selected = manager.onIoSelected.bind(manager);
+
+ const templateOptions = this._findTemplateOptions(options.id, toolbox) as ContainerFlowBlockOptions;
+ options.builder = templateOptions.builder;
+ options.gen_tree = templateOptions.gen_tree;
+
+ const block = new ContainerFlowBlock(options, blockId, toolbox.uiSignalService);
+ block.blockData = Object.assign({}, data.value.extra);
+
+ return block;
+ }
+
+ serialize(): FlowBlockData {
+ return Object.assign(super.serialize(), { subtype: BLOCK_TYPE });
+ }
+
+ public getBodyArea(): Area2D {
+ const rect = this.handler.getBodyElement().getBBox();
+ return {
+ x: this.position.x,
+ y: this.position.y,
+ width: rect.width,
+ height: rect.height,
+ }
+ }
+
+ public moveBy(distance: {x: number, y: number}) {
+
+ const dragged = super.moveBy(distance);
+
+ return dragged.concat(this.moveContents(distance));
+ }
+
+ public moveContents(distance: Position2D) {
+ let result = this.contents.concat([]);
+ for (const block of this.contents) {
+ const dragged = block.moveBy(distance);
+ if (dragged.length > 0) {
+ result = result.concat(dragged);
+ }
+ }
+
+ return result;
+ }
+
+ public endMove(): FlowBlock[] {
+ const movement = this.handler.dropOnEndMove();
+ return this.moveBy(movement);
+ }
+
+ onGetFocus() {
+ this.handler.onGetFocus();
+ }
+
+ onLoseFocus() {
+ this.handler.onLoseFocus();
+ }
+
+ // Container-related
+ updateContainer(container: FlowBlock) {
+ this.handler.updateContainer(container as (UiFlowBlock | null));
+ }
+
+ recursiveGetAllContents(): FlowBlock[] {
+ let acc: FlowBlock[] = [];
+
+ for (const content of this.contents) {
+ if (content instanceof ContainerFlowBlock) {
+ acc = acc.concat(content.recursiveGetAllContents());
+ }
+ acc.push(content);
+ }
+
+ return acc;
+ }
+
+ repositionContents(){
+ this.handler.repositionContents();
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/dom_utils.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/dom_utils.ts
new file mode 100644
index 00000000..52f14f22
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/dom_utils.ts
@@ -0,0 +1,141 @@
+import { UnderlineSettings } from "../../dialogs/configure-link-dialog/configure-link-dialog.component";
+
+export function isTagOnAncestors(node: Node, tag: string): null | {tags: string[], ancestor: HTMLElement} {
+ if (!(node instanceof HTMLElement)) {
+ return isTagOnAncestors(node.parentElement, tag);
+ }
+
+ let element = node as HTMLElement;
+ const tags = [];
+
+ while (element) {
+ if (element.tagName.toLowerCase() === 'foreignobject') {
+ return null;
+ }
+ else if (element.tagName.toLowerCase() === tag) {
+ tags.reverse();
+ return {
+ tags,
+ ancestor: element,
+ };
+ }
+ else {
+ tags.push(element.tagName.toLowerCase());
+ element = element.parentElement;
+ }
+ }
+
+ return null;
+}
+
+export function isTagOnTree(node: Node, tag: string): null | HTMLElement {
+ if (node instanceof HTMLElement) {
+ if (node.tagName.toLowerCase() === tag) {
+ return node;
+ }
+ }
+
+ for (const child of Array.from(node.childNodes)) {
+ const inChild = isTagOnTree(child, tag);
+ if (inChild) {
+ return inChild;
+ }
+ }
+
+ return null;
+}
+
+
+export function extractContentsToRight(element: HTMLElement) {
+ const parent = element.parentNode;
+ const next = element.nextSibling;
+ for (const node of Array.from(element.childNodes)) {
+ if (next) {
+ parent.insertBefore(node, next);
+ }
+ else {
+ parent.appendChild(node);
+ }
+ }
+ parent.removeChild(element);
+}
+
+export function surroundRangeWithElement(range: Range, element: HTMLElement) {
+
+ // The difference with Range.surroundContents() is that this supports
+ // ranges that start at one tag and end at another.
+ // In exchange it has to clone whole tags, not supporting partial ones.
+
+
+ const contents = range.extractContents();
+ element.appendChild(contents);
+ range.insertNode(element);
+}
+
+
+// Taken from: https://stackoverflow.com/a/3627747
+export function colorToHex(rgb: string): string {
+ if (/^#[0-9A-F]{3,6}$/i.test(rgb)) {
+ return rgb;
+ }
+
+ const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
+ if (!match) {
+ return null;
+ }
+ function hex(x: string) {
+ return ("0" + parseInt(x).toString(16)).slice(-2);
+ }
+ return "#" + hex(match[1]) + hex(match[2]) + hex(match[3]);
+}
+
+export function getUnderlineSettings(tag: HTMLAnchorElement): UnderlineSettings {
+ if (tag.style.textDecoration === 'none') {
+ return 'none';
+ }
+ else if (tag.style.textDecorationColor && tag.style.textDecorationColor != 'revert') {
+ const hex = colorToHex(tag.style.textDecorationColor);
+ if (!hex) {
+ console.warn("Error parsing underline color", tag.style.textDecorationColor);
+ }
+ return { color: hex || '#000000' };
+ }
+ else {
+ return 'default';
+ }
+}
+
+export function applyUnderlineSettings(tag: HTMLAnchorElement, underline: UnderlineSettings) {
+ if ((underline === 'default') || (!underline)) {
+ tag.style.textDecoration = 'revert';
+ tag.style.textDecorationColor = 'revert';
+ }
+ else if (underline === 'none') {
+ tag.style.textDecoration = 'none';
+ }
+ else {
+ tag.style.textDecoration = 'revert';
+ tag.style.textDecorationColor = underline.color;
+ }
+}
+
+function flattenTag(element: Node) {
+ const parent = element.parentNode;
+ const next = element.nextSibling;
+ for (const node of Array.from(element.childNodes)) {
+ if (next) {
+ parent.insertBefore(node, next);
+ }
+ else {
+ parent.appendChild(node);
+ }
+ }
+ parent.removeChild(element);
+}
+
+export function flattenAllTagsUnder(root: HTMLElement, tagNameToFlatten: string) {
+ const toFlatten = root.querySelectorAll(tagNameToFlatten);
+ for (const tag of Array.from(toFlatten)) {
+ flattenTag(tag);
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/dynamic_text.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/dynamic_text.ts
new file mode 100644
index 00000000..368a1898
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/dynamic_text.ts
@@ -0,0 +1,306 @@
+import { Subscription } from "rxjs";
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { DirectValue } from "../../direct_value";
+import { FlowBlock, Area2D } from "../../flow_block";
+import { UiFlowBlock, UiFlowBlockBuilder, UiFlowBlockHandler, TextEditable, TextReadable, UiFlowBlockBuilderInitOps } from "../ui_flow_block";
+import { UiElementHandle, HandleableElement, ConfigurableSettingsElement } from "./ui_element_handle";
+import { BlockAllowedConfigurations, BlockConfigurationOptions } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+const DefaultContent = "- Dynamic text -";
+const DEFAULT_BACKGROUND_COLOR = '#222';
+const DEFAULT_TEXT_COLOR = '#fc4';
+
+export const DynamicTextBuilder: UiFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: UiFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const output = new DynamicText(canvas, group, block, service, initOps);
+ output.init();
+ return output;
+}
+
+class DynamicText implements UiFlowBlockHandler, ConfigurableSettingsElement, HandleableElement {
+ private subscription: Subscription;
+ private textBox: SVGTextElement;
+ private rect: SVGRectElement;
+ private rectShadow: SVGRectElement;
+ private handle: UiElementHandle;
+ readonly MinWidth = 120;
+ isStaticText: boolean;
+ private value: string;
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ private block: UiFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ const node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.rectShadow = document.createElementNS(SvgNS, 'rect');
+
+ group.setAttribute('class', 'flow_node ui_node output_node');
+
+ this.textBox = document.createElementNS(SvgNS, 'text');
+ this.textBox.setAttribute('class', 'output_text');
+ this.textBox.setAttributeNS(null, 'textlength', '100%');
+
+ this.value = DefaultContent;
+
+ node.appendChild(this.rectShadow);
+ node.appendChild(this.rect);
+ node.appendChild(this.textBox);
+ group.appendChild(node);
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.rectShadow.setAttributeNS(null, 'class', "body_shadow");
+ this.rectShadow.setAttributeNS(null, 'x', "0");
+ this.rectShadow.setAttributeNS(null, 'y', "0");
+
+ this.updateStyle();
+ this._updateText();
+ this._updateSize();
+
+ if (initOps.workspace) {
+ this.handle = new UiElementHandle(this, node, initOps.workspace, ['adjust_settings']);
+ }
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return true;
+ }
+
+ get text(): string {
+ return this.value;
+ }
+
+ dispose() {
+ return () => this.subscription.unsubscribe();
+ }
+
+ onInputUpdated(connectedBlock: FlowBlock, inputIndex: number) {
+ this.isStaticText = connectedBlock instanceof DirectValue;
+ if (connectedBlock instanceof DirectValue) {
+ this.onConnectionValueUpdate(inputIndex, connectedBlock.value);
+ }
+ }
+
+ onConnectionLost(portIndex: number) {
+ this.onConnectionValueUpdate(portIndex, DefaultContent);
+ }
+
+ onConnectionValueUpdate(_inputIndex: number, value: string) {
+ this.value = value;
+ this._updateText();
+ this._updateSize();
+ }
+
+ init() {
+ const observer = this.service.onElementUpdate(this.block.options.id, this.block.id);
+
+ this.subscription = observer.subscribe({
+ next: (value: any) => {
+ this.onConnectionValueUpdate(0, JSON.stringify(value.values[0]));
+ }
+ });
+
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+
+ // Focus management
+ onClick() {
+ // TODO: Double click for edition?
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ // Handleable element
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ updateOptions() {
+ this._applyConfiguration(this.block.blockData.settings || {});
+ }
+
+ // Configurable element
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions) {
+ const settingsStorage = Object.assign({}, this.block.blockData.settings || {});
+
+ if (settings.text) {
+ if (!settingsStorage.text) {
+ settingsStorage.text = {};
+ }
+
+ if (settings.text.color) {
+ settingsStorage.text.color = {value: settings.text.color.value};
+ }
+ if (settings.text.fontSize) {
+ settingsStorage.text.fontSize = {value: settings.text.fontSize.value};
+ }
+ }
+
+ settingsStorage.bg = settings.bg;
+
+ this.block.blockData.settings = settingsStorage;
+ this.updateStyle();
+ this._updateSize({ anchor: 'bottom-center' }); // Style changes might change the block's size
+
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+ this.block.notifyOptionsChange();
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ const config = Object.assign({}, this.block.blockData.settings || {});
+
+ // Seed default configuration if not already there
+ if (!config.bg) {
+ config.bg = { type: 'color', value: DEFAULT_BACKGROUND_COLOR };
+ }
+ if (!config.text) {
+ config.text = {};
+ }
+ if (!config.text.color) {
+ config.text.color = {value: DEFAULT_TEXT_COLOR};
+ }
+
+ return config;
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return {
+ text: {
+ color: true,
+ fontSize: true,
+ },
+ background: {
+ color: true,
+ image: false,
+ }
+ };
+ }
+
+ // Style management
+ updateStyle() {
+ const settings = this.block.blockData.settings;
+ if (!settings) {
+ return;
+ }
+
+ if (settings.text) {
+ if (settings.text.color) {
+ this.textBox.style.fill = settings.text.color.value;
+ }
+ if (settings.text.fontSize) {
+ this.textBox.style.fontSize = settings.text.fontSize.value + 'px';
+ }
+ }
+
+ if (settings.bg) {
+ // Get color to apply
+ let color = DEFAULT_BACKGROUND_COLOR;
+ if (settings.bg.type === 'color') {
+ color = settings.bg.value;
+ }
+ else if (settings.bg.type === 'transparent') {
+ color = 'transparent';
+ }
+
+ // The shadow creates unexpected effects with transparent
+ // backgrounds, better to just remove it
+ if (color === 'transparent') {
+ this.rectShadow.classList.add('hidden');
+ }
+ else {
+ this.rectShadow.classList.remove('hidden');
+ }
+
+ // Apply it to the element's background
+ this.rect.style.fill = color;
+ }
+ }
+
+ // Aux
+ _updateText() {
+ this.textBox.innerHTML = '';
+
+ const lines = this.value.split('\n')
+ for (let line of lines) {
+ if (line.length === 0) {
+ line = ' '
+ }
+ const span = document.createElementNS(SvgNS, 'tspan');
+ span.setAttributeNS(null, 'x', '0');
+ span.setAttributeNS(null, 'dy', '1.2em');
+ span.textContent = line;
+
+ this.textBox.appendChild(span);
+ }
+ }
+
+ _updateSize(opts?: { anchor?: 'bottom-center' | 'top-left' }) {
+ const textArea = this.textBox.getBoundingClientRect();
+
+ const box_height = textArea.height * 1.5;
+ const box_width = Math.max(textArea.width + 50, this.MinWidth);
+
+ this.textBox.setAttributeNS(null, 'y', (box_height - textArea.height)/2 + "");
+ for (const line of Array.from(this.textBox.childNodes)) {
+ if (line instanceof SVGTSpanElement) {
+ line.setAttributeNS(null, 'x', (box_width - textArea.width)/2 + "");
+ }
+ }
+
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+ this.rectShadow.setAttributeNS(null, 'height', box_height + "");
+ this.rectShadow.setAttributeNS(null, 'width', box_width + "");
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/fixed_image.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/fixed_image.ts
new file mode 100644
index 00000000..98725cc6
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/fixed_image.ts
@@ -0,0 +1,232 @@
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { Area2D, FlowBlock, Resizeable } from "../../flow_block";
+import { TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilder, UiFlowBlockBuilderInitOps, UiFlowBlockHandler } from "../ui_flow_block";
+import { ConfigurableSettingsElement, HandleableElement, UiElementHandle } from "./ui_element_handle";
+import { BlockConfigurationOptions, BlockAllowedConfigurations } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+const DefaultImageUrl = "/assets/logo-dark.png";
+
+export const FixedImageBuilder: UiFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: UiFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const element = new FixedImage(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class FixedImage implements UiFlowBlockHandler, ConfigurableSettingsElement, HandleableElement, Resizeable {
+ private imageBox: SVGImageElement;
+ private rect: SVGRectElement;
+ private handle: UiElementHandle;
+ private width: number;
+ private height: number;
+
+ private readonly minWidth = 100;
+ private readonly minHeight = 100;
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ private block: UiFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ const node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ const contentsGroup = document.createElementNS(SvgNS, 'g');
+
+ group.setAttribute('class', 'flow_node ui_node image_node');
+
+ this.imageBox = document.createElementNS(SvgNS, 'image');
+ this.imageBox.setAttribute('class', 'image');
+
+
+ const settings = block.blockData.settings;
+ let imageUrl = DefaultImageUrl;
+ if (settings && settings.body && settings.body.image) {
+ imageUrl = this.block.workspace.getAssetUrlOnProgram(block.blockData.settings.body.image.id);
+ }
+ this.imageBox.setAttributeNS(null, 'href', imageUrl);
+
+ this.imageBox.setAttributeNS(null, 'width', this.minWidth + '');
+ this.imageBox.setAttributeNS(null, 'height', this.minHeight + '');
+
+ contentsGroup.appendChild(this.imageBox);
+ node.appendChild(this.rect);
+ node.appendChild(this.imageBox);
+ group.appendChild(node);
+
+ if (this.block.blockData.dimensions) {
+ this.height = this.block.blockData.dimensions.height;
+ this.width = this.block.blockData.dimensions.width;
+ }
+ else {
+ this.height = this.minHeight;
+ this.width = this.minWidth;
+ }
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.updateStyle();
+ this._updateSize();
+
+ if (initOps.workspace) {
+ this.handle = new UiElementHandle(this, node, initOps.workspace, ['adjust_settings', 'resize_width_height']);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+
+ getArea(): Area2D {
+ return this.rect.getBBox();
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return false;
+ }
+
+ get isStaticText(): boolean {
+ return false;
+ }
+
+ dispose() {}
+
+ onInputUpdated(block: FlowBlock, inputIndex: number) {}
+
+ onConnectionValueUpdate(inputIndex: number, value: string) {}
+
+ onConnectionLost(portIndex: number) {
+ this.onConnectionValueUpdate(portIndex, DefaultImageUrl);
+ }
+
+ // Focus management
+ onClick() {
+ // TODO: Double click for edition?
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ // Resizeable
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ // Resizeable
+ resize(dim: { width: number; height: number; }) {
+ // Check that what the minimum available size is
+
+ this.width = Math.max(this.minWidth, dim.width);
+ this.height = Math.max(this.minHeight, dim.height);
+
+ this.block.blockData.dimensions = { width: this.width, height: this.height };
+
+ this._updateSize();
+ this.handle.update();
+ this.block.notifyOptionsChange();
+ }
+
+ // Handleable element
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ updateOptions() {
+ this._applyConfiguration(this.block.blockData.settings || {});
+
+ if (this.block.blockData.dimensions) {
+ this.height = this.block.blockData.dimensions.height;
+ this.width = this.block.blockData.dimensions.width;
+ this._updateSize();
+ }
+ }
+
+ // Configurable element
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions): void {
+ const settingsStorage = Object.assign({}, this.block.blockData.settings || {});
+
+ if (settings.body && settings.body.image) {
+ const imageUrl = this.block.workspace.getAssetUrlOnProgram(settings.body.image.id);
+ this.imageBox.setAttributeNS(null, 'href', imageUrl);
+
+ if (!settingsStorage.body) {
+ settingsStorage.body = {};
+ }
+ settingsStorage.body.image = { id: settings.body.image.id };
+ }
+
+ this.block.blockData.settings = settingsStorage;
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+
+ this.block.notifyOptionsChange();
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ return Object.assign({}, this.block.blockData.settings || {});
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return {
+ body: {
+ image: true,
+ },
+ };
+ }
+
+ // Style management
+ updateStyle() {
+ const settings = this.block.blockData.settings;
+ if (!settings) {
+ return;
+ }
+ }
+
+ // Aux
+ _updateSize() {
+ this.imageBox.setAttributeNS(null, 'width', this.width + "");
+ this.imageBox.setAttributeNS(null, 'height', this.height + "");
+
+ this.rect.setAttributeNS(null, 'height', this.height + "");
+ this.rect.setAttributeNS(null, 'width', this.width + "");
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/fixed_text.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/fixed_text.ts
new file mode 100644
index 00000000..29bc2460
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/fixed_text.ts
@@ -0,0 +1,395 @@
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { Area2D, FlowBlock } from "../../flow_block";
+import { TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilder, UiFlowBlockBuilderInitOps, UiFlowBlockHandler, Autoresizable } from "../ui_flow_block";
+import { ConfigurableSettingsElement, HandleableElement, UiElementHandle } from "./ui_element_handle";
+import { BlockConfigurationOptions, BlockAllowedConfigurations, fontWeightToCss } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+import { ContainerFlowBlock } from "../container_flow_block";
+import { startOnElementEditor, FormattedTextTree, formattedTextTreeToDom } from "./utils";
+import { FlowWorkspace } from "../../flow_workspace";
+
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+export const MAX_WIDTH = 1024;
+
+const DefaultContent = { type: 'text', value: "- Static (editable) text -"};
+
+export const FixedTextBuilder: UiFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: UiFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const element = new FixedText(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class FixedText implements UiFlowBlockHandler, TextEditable, ConfigurableSettingsElement, HandleableElement {
+ private textBox: SVGForeignObjectElement;
+ private textValue: FormattedTextTree;
+ private rect: SVGRectElement;
+ readonly MinWidth = 200;
+ readonly MinHeight = 140;
+ private handle: UiElementHandle;
+ private _container: ContainerFlowBlock;
+ private contentBox: HTMLDivElement;
+ private editing = false;
+
+ private readonly workspace: FlowWorkspace;
+ private fullTextArea: Area2D;
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ private block: UiFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ const node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ const contentsGroup = document.createElementNS(SvgNS, 'g');
+
+ group.setAttribute('class', 'flow_node ui_node text_node');
+
+ this.textBox = document.createElementNS(SvgNS, 'foreignObject');
+ this.textBox.setAttribute('class', 'text');
+
+ if ((block.blockData.textContent) && !(block.blockData.content)) {
+ block.blockData.content = [{ type: 'text', value: block.blockData.textContent }];
+ }
+ this.textValue = block.blockData.content || [DefaultContent];
+
+ contentsGroup.appendChild(this.textBox);
+ node.appendChild(this.rect);
+ node.appendChild(this.textBox);
+ group.appendChild(node);
+
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.updateStyle();
+ this._updateTextBox();
+ this._updateSize();
+
+ if (initOps.workspace) {
+ this.workspace = initOps.workspace;
+ this.handle = new UiElementHandle(this, node, initOps.workspace, ['adjust_settings']);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+
+ getArea(): Area2D {
+ return this.getBodyElement().getBBox();
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return true;
+ }
+
+ get isStaticText(): boolean {
+ return true;
+ }
+
+ get editableTextName(): string {
+ return 'contents';
+ }
+
+ public get text(): string {
+ return this.contentBox.innerText;
+ }
+
+ dispose() {}
+
+ onInputUpdated(block: FlowBlock, inputIndex: number) {}
+
+ onConnectionValueUpdate(inputIndex: number, value: string) {}
+
+ onConnectionLost(portIndex: number) { }
+
+ // Focus management
+ onClick() {
+ // TODO: Double click for edition?
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (this.contentBox) {
+ this.contentBox.blur();
+ }
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ // Handleable element
+ doesTakeAllHorizontal() {
+ return false;
+ }
+
+ isAutoresizable(): this is Autoresizable {
+ return false;
+ }
+
+ getMinSize() {
+ return { width: this.MinWidth, height: this.MinHeight };
+ }
+
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ updateOptions() {
+ if ((this.block.blockData.textContent) && !(this.block.blockData.content)) {
+ this.block.blockData.content = [{ type: 'text', value: this.block.blockData.textContent }];
+ }
+ this.textValue = this.block.blockData.content || [DefaultContent];
+
+ this._updateTextBox();
+ this._updateSize();
+ this._applyConfiguration(this.block.blockData.settings || {});
+ }
+
+ // Configurable element
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions): void {
+ const settingsStorage = Object.assign({}, this.block.blockData.settings || {});
+
+ if (settings.text) {
+
+ if (!settingsStorage.text) {
+ settingsStorage.text = {};
+ }
+
+ if (settings.text.color) {
+ settingsStorage.text.color = {value: settings.text.color.value};
+ }
+ if (settings.text.fontSize) {
+ settingsStorage.text.fontSize = {value: settings.text.fontSize.value};
+ }
+ if (settings.text.fontWeight) {
+ settingsStorage.text.fontWeight = {value: settings.text.fontWeight.value};
+ }
+ }
+
+ this.block.blockData.settings = settingsStorage;
+ this.updateStyle();
+ this._updateSize({ anchor: 'bottom-center' }); // Style changes might change the block's size
+
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+
+ this.block.notifyOptionsChange();
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ return Object.assign({}, this.block.blockData.settings || {});
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return {
+ text: {
+ color: true,
+ fontSize: true,
+ fontWeight: true,
+ },
+ };
+ }
+
+ // When inside a container, avoid overflowing it
+ updateContainer(container: UiFlowBlock | null) {
+ if (container instanceof ContainerFlowBlock) {
+ this._container = container;
+ }
+ else {
+ this._container = null;
+ }
+ this._updateSize();
+ }
+
+ dropOnEndMove() {
+ if (!this.editing) {
+ this._updateTextBox();
+ this._updateSize();
+ }
+ return { x: 0, y: 0 };
+ }
+
+ // Style management
+ updateStyle() {
+ const settings = this.block.blockData.settings;
+ if (!settings) {
+ return;
+ }
+
+ if (settings.text) {
+ if (settings.text.color) {
+ this.textBox.style.color = settings.text.color.value;
+ }
+ if (settings.text.fontSize) {
+ this.textBox.style.fontSize = settings.text.fontSize.value + 'px';
+ }
+ if (settings.text.fontWeight) {
+ this.textBox.style.fontWeight = fontWeightToCss(settings.text.fontWeight.value);
+ }
+ }
+ }
+
+ // Aux
+ onContentEditStart() {
+ this.textBox.setAttributeNS(null, 'y', "");
+ this.textBox.setAttributeNS(null, 'x', "");
+
+ const width = this.rect.getAttributeNS(null, 'width');
+ const height = this.rect.getAttributeNS(null, 'height');
+ this.textBox.setAttributeNS(null, 'width', width);
+ this.textBox.setAttributeNS(null, 'height', height);
+
+ this.contentBox.style.height = height + 'px';
+ this.contentBox.style.maxWidth = '';
+ this.contentBox.style.width = width + 'px';
+ this.contentBox.style.height = height + 'px';
+ this.contentBox.classList.add('editing');
+
+ this.editing = true;
+
+ startOnElementEditor(this.contentBox, this.textBox, this.block.workspace.getDialog(),
+ (tt: FormattedTextTree) => {
+ this.block.blockData.content = this.textValue = tt;
+
+ this.editing = false;
+ this._updateTextBox();
+ this._updateSize();
+ if (this.workspace) {
+ this.workspace.invalidateBlock(this.block.id);
+ }
+ },
+ (width: number, height: number) => {
+ width = Math.min(MAX_WIDTH, Math.max(width, this.MinWidth));
+ height = Math.max(height, this.MinHeight);
+
+ const zoom = this.workspace ? this.workspace.getInvZoomLevel() : 1;
+
+ this.rect.setAttributeNS(null, 'width', width * zoom + '');
+ this.rect.setAttributeNS(null, 'height', height * zoom + '');
+ this.textBox.setAttributeNS(null, 'width', width * zoom + '');
+ this.textBox.setAttributeNS(null, 'height', height * zoom + '');
+
+ this.contentBox.style.width = width * zoom + 'px';
+ this.contentBox.style.height = height * zoom + 'px';
+ });
+ }
+
+ _updateTextBox() {
+ this.textBox.innerHTML = '';
+
+ this.contentBox = document.createElement('div');
+ this.contentBox.style.width = 'max-content';
+
+ if (this.initOps.workspace) {
+ // Don't make editable on exhibitor
+ this.contentBox.contentEditable = 'true';
+ }
+ this.contentBox.onfocus = this.onContentEditStart.bind(this);
+ this.contentBox.onmousedown = (ev: MouseEvent) => {
+ ev.stopImmediatePropagation();
+ }
+
+ const container = document.createElement('div');
+ const content = formattedTextTreeToDom(this.textValue);
+
+ container.appendChild(content);
+ this.contentBox.appendChild(container);
+
+ // Give all available width
+ this.contentBox.style.maxWidth = MAX_WIDTH + 'px';
+ this.contentBox.style.width = 'max-content';
+
+ this.textBox.setAttributeNS(null, 'width', MAX_WIDTH + '');
+
+ // Then add it to the ForeignObject
+ this.textBox.appendChild(this.contentBox);
+
+ this._updateFullTextArea();
+ }
+
+ _updateFullTextArea() {
+ const textArea = this.contentBox.getBoundingClientRect();
+ const zoom = this.workspace ? this.workspace.getInvZoomLevel() : 1;
+
+ this.fullTextArea = {
+ x: textArea.x,
+ y: textArea.y,
+ width: textArea.width * zoom,
+ height: textArea.height * zoom,
+ };
+ }
+
+ _updateSize(opts?: { anchor?: 'bottom-center' | 'top-left' }) {
+ // Obtain size taken by all the text
+ const zoom = this.workspace ? this.workspace.getInvZoomLevel() : 1;
+ const anchor = opts && opts.anchor ? opts.anchor : 'top-left';
+
+ const oldHeight = this.rect.height.baseVal.value;
+ const oldWidth = this.rect.width.baseVal.value;
+ const box_height = Math.max(this.fullTextArea.height + 50 * zoom, this.MinHeight);
+ const box_width = Math.min(MAX_WIDTH, Math.max(this.fullTextArea.width + 50 * zoom, this.MinWidth));
+
+ if (anchor === 'bottom-center') {
+ // Move the box around to respect the anchor point
+ this.block.moveBy({
+ x: -((box_width - oldWidth) / 2),
+ y: -(box_height - oldHeight),
+ })
+ }
+
+ this.textBox.setAttributeNS(null, 'x', (box_width - this.fullTextArea.width)/2 + "");
+ this.textBox.setAttributeNS(null, 'y', (box_height - this.fullTextArea.height)/2 + "");
+ this.textBox.setAttributeNS(null, 'width', box_width + "");
+ this.textBox.setAttributeNS(null, 'height', this.fullTextArea.height + "");
+
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/horizontal_separator.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/horizontal_separator.ts
new file mode 100644
index 00000000..d173ccc6
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/horizontal_separator.ts
@@ -0,0 +1,310 @@
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { BlockAllowedConfigurations, BlockConfigurationOptions } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+import { Area2D, FlowBlock } from "../../flow_block";
+import { TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilder, UiFlowBlockBuilderInitOps, UiFlowBlockHandler, Autoresizable } from "../ui_flow_block";
+import { UiElementHandle, ConfigurableSettingsElement } from "./ui_element_handle";
+import { ContainerFlowBlock } from "../container_flow_block";
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+const Label = "- Horizontal separator -";
+
+export const HorizontalSeparatorBuilder: UiFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: UiFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const element = new HorizontalSeparator(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class HorizontalSeparator implements UiFlowBlockHandler, Autoresizable, ConfigurableSettingsElement {
+ private textBox: SVGTextElement;
+ private rect: SVGRectElement;
+ private handle: UiElementHandle;
+ private _container: ContainerFlowBlock;
+ private separatorPath: SVGPathElement;
+ private readonly _minSize: { width: number, height: number };
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ private block: UiFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ const node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ const contentsGroup = document.createElementNS(SvgNS, 'g');
+ this.separatorPath = document.createElementNS(SvgNS, 'path');
+
+ group.setAttribute('class', 'flow_node ui_node separator_node');
+
+ this.textBox = document.createElementNS(SvgNS, 'text');
+ this.textBox.setAttribute('class', 'text');
+ this.textBox.setAttributeNS(null, 'textlength', '100%');
+
+ this.textBox.textContent = Label;
+
+ this.separatorPath.setAttribute('class', 'representation');
+
+ contentsGroup.appendChild(this.textBox);
+ node.appendChild(this.rect);
+ node.appendChild(this.separatorPath);
+ node.appendChild(this.textBox);
+ group.appendChild(node);
+
+ this._minSize = this.textBox.getBBox();
+ this._minSize.width += 50;
+ this._minSize.height *= 1.5;
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.separatorPath.setAttributeNS(null, 'x', "0");
+ this.separatorPath.setAttributeNS(null, 'y', "0");
+
+ this.updateStyle();
+ this._updateSize();
+
+ if (initOps.workspace) {
+ this.handle = new UiElementHandle(this, node, initOps.workspace, ['adjust_settings']);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+ getArea(): Area2D {
+ return this.rect.getBBox();
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return false;
+ }
+
+ get isStaticText(): boolean {
+ return false;
+ }
+
+ dispose() {}
+
+ onInputUpdated(block: FlowBlock, inputIndex: number) {}
+
+ onConnectionValueUpdate(inputIndex: number, value: string) {}
+
+ onConnectionLost(portIndex: number) {}
+
+ // Focus management
+ onClick() {}
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ // Resizing
+ readonly isNotHorizontallyStackable = true;
+ isAutoresizable(): this is Autoresizable {
+ return true;
+ }
+
+ doesTakeAllHorizontal() {
+ return true;
+ }
+
+ doesTakeAllVertical() {
+ return false;
+ }
+
+ resize(_dims: any) {
+ this._updateSize();
+ }
+
+ getMinSize() {
+ return {
+ width: this._minSize.width,
+ height: this._minSize.height,
+ }
+ }
+
+ // Handleable element
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ updateOptions() {
+ this._applyConfiguration(this.block.blockData.settings || {});
+ }
+
+ // Configurable element
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions): void {
+ const settingsStorage = Object.assign({}, this.block.blockData.settings || {});
+
+ if (!settingsStorage.body) {
+ settingsStorage.body = {};
+ }
+
+ if (settings.body) {
+ if (settings.body.widthTaken) {
+ settingsStorage.body.widthTaken = { value: settings.body.widthTaken.value };
+ }
+ }
+
+ this.block.blockData.settings = settingsStorage;
+ this.updateStyle();
+ this._updateSize(); // Style changes might change the block's size
+
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+
+ this.block.notifyOptionsChange();
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ const config = Object.assign({}, this.block.blockData.settings || {});
+
+ if (!config.body) {
+ config.body = {};
+ }
+ if (!config.body.widthTaken) {
+ config.body.widthTaken = {value: 'half'};
+ }
+
+ return config;
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return {
+ body: {
+ widthTaken: [
+ { name: 'short', style: '20%' },
+ { name: 'half', style: '50%' },
+ { name: 'full', style: '100%' },
+ ],
+ },
+ };
+ }
+
+ // When inside a container, push to the right and cover all width
+ updateContainer(container: UiFlowBlock | null) {
+ if (container instanceof ContainerFlowBlock) {
+ this._container = container;
+ }
+ else {
+ this._container = null;
+ }
+ this._updateSize();
+ }
+
+ dropOnEndMove() {
+ if (!this._container) {
+ return {x: 0, y: 0};
+ }
+
+ let result = {x: 0, y: 0};
+
+ const parentArea = this._container.getBodyArea();
+ const offset = this.block.getOffset();
+ const xdiff = offset.x - (parentArea.x);
+ if (xdiff != 0) {
+ result = { x: -xdiff, y: 0};
+ }
+
+ this._updateSize();
+ return result;
+ }
+
+ // Style management
+ updateStyle() {
+ const settings = this.block.blockData.settings;
+ if (!settings) {
+ return;
+ }
+
+ // Nothing to do here, as the style change will be appreciated when
+ // `_updateSize()` is called.
+ }
+
+ // Aux
+ _updateSize() {
+ const textArea = this.textBox.getBoundingClientRect();
+
+ const box_height = this._minSize.height;
+ let box_width = this._minSize.width;
+
+ if (this._container) {
+ const parentArea = this._container.getBodyArea();
+ box_width = parentArea.width;
+ }
+
+ this.textBox.setAttributeNS(null, 'y', box_height/2 + "");
+ this.textBox.setAttributeNS(null, 'x', (box_width - textArea.width)/2 + "");
+
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+
+
+ this.separatorPath.setAttributeNS(null, 'height', box_height + "");
+ this.separatorPath.setAttributeNS(null, 'width', box_width + "");
+
+ const settings = this.block.blockData.settings;
+ let widthTaken = 'half';
+ if (settings && settings.body && settings.body.widthTaken) {
+ widthTaken = settings.body.widthTaken.value;
+ }
+
+ if (widthTaken == 'short') {
+ this.separatorPath.setAttributeNS(null, 'd', `M${(box_width / 10) * 4},${box_height / 1.25} H${(box_width / 10) * 6}`);
+ }
+ else if (widthTaken === 'full') {
+ this.separatorPath.setAttributeNS(null, 'd', `M0,${box_height / 1.25} H${box_width}`);
+ }
+ else {
+ this.separatorPath.setAttributeNS(null, 'd', `M${box_width / 4},${box_height / 1.25} H${(box_width / 4) * 3}`);
+ }
+
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/horizontal_ui_section.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/horizontal_ui_section.ts
new file mode 100644
index 00000000..bfa23612
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/horizontal_ui_section.ts
@@ -0,0 +1,474 @@
+import { Subscription } from "rxjs";
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { BlockAllowedConfigurations, BlockConfigurationOptions } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+import { Area2D, FlowBlock } from "../../flow_block";
+import { ContainerFlowBlock, ContainerFlowBlockBuilder, ContainerFlowBlockHandler, GenTreeProc } from "../container_flow_block";
+import { TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilderInitOps, UiFlowBlockHandler, Autoresizable } from "../ui_flow_block";
+import { ConfigurableSettingsElement, HandleableElement, UiElementHandle } from "./ui_element_handle";
+import { CutTree } from "./ui_tree_repr";
+import { combinedArea } from "./utils";
+import { PositionHorizontalContents, PositionVerticalContents, SEPARATION, GetMinSizeHorizontal, GetMinSizeVertical } from "./positioning";
+import { CannotSetAsContentsError } from "../cannot_set_as_contents_error";
+
+const SvgNS = "http://www.w3.org/2000/svg";
+const BLOCK_TYPE_ANNOTATION = 'Section'
+const DEFAULT_COLOR = '';
+
+const MIN_HEIGHT = SEPARATION;
+const MIN_WIDTH = SEPARATION;
+
+export const HorizontalUiSectionBuilder: ContainerFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: ContainerFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const element = new HorizontalUiSection(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class HorizontalUiSection implements ContainerFlowBlockHandler, HandleableElement, Autoresizable, ConfigurableSettingsElement {
+ subscription: Subscription;
+ handle: UiElementHandle | null = null;
+ node: SVGGElement;
+ rect: SVGRectElement;
+ grid: SVGGElement;
+ width: number;
+ height: number;
+ placeholder: SVGTextElement;
+ container: ContainerFlowBlock;
+ private _contents: FlowBlock[] = [];
+ nestedHorizontal: boolean;
+ private readonly freeWidth: number;
+ private readonly freeHeight: number;
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ public block: ContainerFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ this.node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.placeholder = document.createElementNS(SvgNS, 'text');
+
+ group.setAttribute('class', 'flow_node ui_node container_node section_node');
+
+ this.grid = document.createElementNS(SvgNS, 'g');
+
+ this.node.appendChild(this.rect);
+ this.node.appendChild(this.grid);
+ this.node.appendChild(this.placeholder);
+
+ group.appendChild(this.node);
+
+
+ this.placeholder.setAttributeNS(null, 'class', 'block_type_annotation');
+ this.placeholder.textContent = BLOCK_TYPE_ANNOTATION;
+
+ const text_width = this.placeholder.getBoundingClientRect().width;
+ const text_height = this.placeholder.getBoundingClientRect().height;
+ const textDim = { width: text_width, height: text_height };
+
+ const bdims = block.blockData.dimensions;
+
+ const minWidth = textDim.width * 1.5;
+ const minHeight = textDim.height * 2;
+ this.freeWidth = this.width = bdims && bdims.width > minWidth ? bdims.width : minWidth;
+ this.freeHeight = this.height = bdims && bdims.height > minHeight ? bdims.height : minHeight;
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.grid.setAttribute('class', 'division_grid');
+
+ this.updateStyle();
+ this._updateInternalElementSizes();
+
+ if (initOps.workspace) {
+ this.handle = new UiElementHandle(this, this.node, initOps.workspace, ['adjust_settings']);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+ _updateInternalElementSizes() {
+ this.rect.setAttributeNS(null, 'width', this.width + '');
+ this.rect.setAttributeNS(null, 'height', this.height + '');
+
+ const textBox = this.placeholder.getBBox();
+ this.placeholder.setAttributeNS(null, 'x', (this.width - textBox.width) / 2 + '');
+ this.placeholder.setAttributeNS(null, 'y', this.height / 2 + textBox.height / 2 + '');
+
+ this.block.blockData.dimensions = { width: this.width, height: this.height };
+ }
+
+ // Resizing
+ doesTakeAllHorizontal() {
+ return !this.nestedHorizontal;
+ }
+
+ doesTakeAllVertical() {
+ return this.nestedHorizontal;
+ }
+
+ isAutoresizable(): this is Autoresizable {
+ return true;
+ }
+
+ getMinSize() {
+ if (this._contents.length === 0) {
+ return { width: MIN_WIDTH, height: MIN_HEIGHT };
+ }
+
+ if (this.nestedHorizontal) {
+ return GetMinSizeVertical(this._contents);
+ }
+ return GetMinSizeHorizontal(this._contents);
+ }
+
+ get isNotHorizontallyStackable() {
+ return this.nestedHorizontal === false;
+ }
+
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ // Resizeable
+ resize(dimensions: { width: number; height: number; }, repositioning?: boolean) {
+ // Make sure what's the minimum possible height
+ const fullContents = this.block.recursiveGetAllContents();
+
+ const pos = this.block.getOffset();
+
+ if (repositioning) {
+ this.width = dimensions.width;
+ this.height = dimensions.height;
+ }
+
+ if (this.nestedHorizontal) {
+ // Resize vertically
+ let minWidth = 0;
+
+ if (fullContents.length > 0) {
+
+ const inflexibleArea = combinedArea(
+ fullContents
+ .filter(b => !(
+ (b instanceof ContainerFlowBlock) || ((b instanceof UiFlowBlock) && b.isAutoresizable())
+ ))
+ .map(b => b.getBodyArea()));
+
+ minWidth = inflexibleArea.width;
+ }
+
+ const newWidth = Math.max(MIN_WIDTH, minWidth, dimensions.width);
+
+ this.width = newWidth;
+ }
+ else {
+ // Resize horizontally
+ let minHeight = 0;
+
+ if (fullContents.length > 0) {
+
+ const inflexibleArea = combinedArea(
+ fullContents
+ .filter(b => !(
+ (b instanceof ContainerFlowBlock) || ((b instanceof UiFlowBlock) && b.isAutoresizable())
+ ))
+ .map(b => b.getBodyArea()));
+
+ minHeight = inflexibleArea.height;
+ }
+
+ const newHeight = Math.max(MIN_HEIGHT, minHeight, dimensions.height);
+
+ this.height = newHeight;
+ }
+
+ this._updateInternalElementSizes();
+ this.block.update();
+
+
+ for (const content of this._contents) {
+ if (content instanceof ContainerFlowBlock) {
+ content.updateContainer(this.block);
+ }
+ else if ((content instanceof UiFlowBlock) && content.isAutoresizable()) {
+ content.updateContainer(this.block);
+ }
+ }
+ }
+
+ // UiFlowBlock
+ onClick() {
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return false;
+ }
+
+ dispose() {}
+
+ onInputUpdated(connectedBlock: FlowBlock, inputIndex: number) {}
+
+ onConnectionLost(portIndex: number) {}
+
+ onConnectionValueUpdate(_inputIndex: number, value: string) {}
+
+ // Container element
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ onContentUpdate(contents: FlowBlock[]) {
+ // Reject elements that cannot be horizontally stacked
+ if (!this.nestedHorizontal) {
+ const problematic = contents.filter((e) => {
+ if (e instanceof ContainerFlowBlock && e.handler instanceof HorizontalUiSection) {
+ return false; // These can always be nested
+ }
+ return (e instanceof UiFlowBlock) && (!e.isHorizontallyStackable())
+ });
+
+ if (problematic.length > 0) {
+ throw new CannotSetAsContentsError("Elements that cannot be horizontally stacked cannot be added to a HorizontalUiSection", problematic);
+ }
+ }
+
+ this._contents = contents;
+ }
+
+ updateContainer(container: UiFlowBlock | null) {
+ if (container instanceof ContainerFlowBlock) {
+ this.container = container;
+ this.nestedHorizontal = (this.container.handler instanceof HorizontalUiSection) && (!this.container.handler.nestedHorizontal);
+ }
+ else {
+ this.container = null;
+
+ this.nestedHorizontal = null;
+ this.width = this.freeWidth;
+ this.height = this.freeHeight;
+ }
+ this._updateInternalElementSizes();
+ }
+
+ repositionContents(): void {
+ if (this._contents.length === 0) {
+ return;
+ }
+
+ const dimensions = this._repositionContents();
+
+ this.resize(dimensions, true);
+ }
+
+ _repositionContents() : { width: number, height: number } {
+ this.stickToContainer();
+
+ if (this.nestedHorizontal) {
+ return PositionVerticalContents(this, this._contents, this.getBodyArea());
+ }
+ else {
+ return PositionHorizontalContents(this, this._contents, this.getBodyArea());
+ }
+ }
+
+ stickToContainer(){
+ if (!this.container) {
+ return;
+ }
+
+ const offset = this.block.getOffset();
+ const area = this.container.getBodyArea();
+ if (this.nestedHorizontal) {
+ const ydiff = offset.y - area.y;
+
+ if (ydiff != 0) {
+ this.block.moveBy({ x: 0, y: -ydiff});
+ }
+ }
+ else {
+ const xdiff = offset.x - area.x;
+
+ if (xdiff != 0) {
+ this.block.moveBy({ x: -xdiff, y: 0});
+ }
+ }
+ }
+
+ dropOnEndMove() {
+ if (!this.container) {
+ return {x: 0, y: 0};
+ }
+
+ let result = {x: 0, y: 0};
+
+ const offset = this.block.getOffset();
+ const area = this.container.getBodyArea();
+ if (this.nestedHorizontal) {
+ // If the parent is an horizontal element, cover all height
+ this.height = area.height;
+
+ const ydiff = offset.y - area.y;
+ if (ydiff) {
+ result = { x: 0, y: -ydiff};
+ }
+ }
+ else {
+ this.width = area.width;
+
+ const xdiff = offset.x - area.x;
+ if (xdiff) {
+ result = { x: -xdiff, y: 0};
+ }
+ }
+
+ this._updateInternalElementSizes();
+
+ if (this._contents.length > 0) {
+ this._repositionContents();
+
+ // If we're repositioning, there's not much to do additionally
+ result = { x: 0, y: 0 };
+ }
+
+ return result;
+ }
+
+ update() {
+ this.onContentUpdate(this._contents);
+ }
+
+ updateOptions() {
+ this._applyConfiguration(this.block.blockData.settings || {});
+
+ if (this.block.blockData.dimensions) {
+ this.height = this.block.blockData.dimensions.height;
+ this.width = this.block.blockData.dimensions.width;
+ this._updateInternalElementSizes();
+ }
+ }
+
+ // Configurable
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return { background: {color: true, image: true} };
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ return Object.assign({}, this.block.blockData.settings || {});
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions): void {
+ if (settings.bg) {
+ this.block.blockData.settings = Object.assign(this.block.blockData.settings || {}, {bg: settings.bg});
+
+ this.updateStyle();
+ }
+
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+
+ this.block.notifyOptionsChange();
+ }
+
+ // Style management
+ updateStyle(){
+ const settings = this.block.blockData.settings;
+ if (!settings) {
+ return;
+ }
+ if (settings.bg) {
+ // Get color to apply
+ let color = DEFAULT_COLOR;
+ if (settings.bg.type === 'color') {
+ color = settings.bg.value;
+ }
+
+ // Apply it to the element's background
+ this.rect.style.fill = color;
+ }
+ }
+
+ // Compilation
+ generateTreeWithGroups(groups: CutTree[]): CutTree {
+ const tree: CutTree = { cut_type: 'hbox', groups: groups, settings: {}, block_id: this.block.id };
+
+ if (this.nestedHorizontal) {
+ // Then this works more like a VBox
+ tree.cut_type = 'vbox';
+ }
+
+ const settings = this.block.blockData.settings;
+ if (settings) {
+ if (settings.bg && settings.bg.type === 'color') {
+ tree.settings.bg = settings.bg;
+ }
+ }
+
+ return tree;
+ }
+}
+
+export const HorizontalUiSectionGenerateTree: GenTreeProc = (handler: UiFlowBlockHandler, blocks: FlowBlock[]) => {
+ const horizHandler = handler as HorizontalUiSection;
+ const filterGroups = (blocks
+ // Get UI blocks
+ .filter(b => b instanceof UiFlowBlock)
+ .map(b => { return { area: b.getBodyArea(), block: b } }));
+
+ if (horizHandler.nestedHorizontal) {
+ // Order by from top to bottom
+ filterGroups.sort((a, b) => a.area.y - b.area.y);
+ }
+ else {
+ // Order by from left to right
+ filterGroups.sort((a, b) => a.area.x - b.area.x);
+ }
+
+ // Render the elements themselves
+ const groups = filterGroups.map((item: {area: Area2D, block: UiFlowBlock}) => item.block.renderAsUiElement());
+
+ // Finally generate the tree
+ return horizHandler.generateTreeWithGroups(groups);
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/link_area.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/link_area.ts
new file mode 100644
index 00000000..50d2164a
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/link_area.ts
@@ -0,0 +1,365 @@
+import { Subscription } from "rxjs";
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { BlockAllowedConfigurations, BlockConfigurationOptions } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+import { Area2D, FlowBlock } from "../../flow_block";
+import { ContainerFlowBlock, ContainerFlowBlockBuilder, ContainerFlowBlockHandler, GenTreeProc } from "../container_flow_block";
+import { Autoresizable, TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilderInitOps, UiFlowBlockHandler } from "../ui_flow_block";
+import { PositionResponsiveContents, SEPARATION, CenterElements } from "./positioning";
+import { getElementsInGroup, getRect, ResponsivePageGenerateTree } from "./responsive_page";
+import { ConfigurableSettingsElement, HandleableElement, UiElementHandle } from "./ui_element_handle";
+import { ContainerElementRepr, CutTree } from "./ui_tree_repr";
+import { combinedArea, listToDict, manipulableAreaToArea2D } from "./utils";
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+const BLOCK_TYPE_ANNOTATION = 'Link Area'
+const SECTION_PADDING = 5;
+
+const MIN_WIDTH = 100;
+const MIN_HEIGHT = 100;
+
+export const LinkAreaBuilder: ContainerFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: ContainerFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const element = new LinkArea(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class LinkArea implements ContainerFlowBlockHandler, HandleableElement, Autoresizable, ConfigurableSettingsElement {
+ subscription: Subscription;
+ handle: UiElementHandle | null = null;
+ node: SVGGElement;
+ rect: SVGRectElement;
+ grid: SVGGElement;
+ width: number;
+ height: number;
+ placeholder: SVGTextElement;
+ container: ContainerFlowBlock;
+ private _contents: FlowBlock[] = [];
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ public block: ContainerFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ this.node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.placeholder = document.createElementNS(SvgNS, 'text');
+
+ group.setAttribute('class', 'flow_node ui_node container_node card_node action_area');
+
+ this.grid = document.createElementNS(SvgNS, 'g');
+
+ this.node.appendChild(this.rect);
+ this.node.appendChild(this.grid);
+ this.node.appendChild(this.placeholder);
+
+ group.appendChild(this.node);
+
+
+ this.placeholder.setAttributeNS(null, 'class', 'block_type_annotation');
+ this.placeholder.textContent = BLOCK_TYPE_ANNOTATION;
+
+ const text_width = this.placeholder.getBoundingClientRect().width;
+ const text_height = this.placeholder.getBoundingClientRect().height;
+ const textDim = { width: text_width, height: text_height };
+
+ const bdims = block.blockData.dimensions;
+ this.width = bdims ? bdims.width : textDim.width * 1.5;
+ this.height = bdims ? bdims.height : textDim.height * 2;
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.grid.setAttribute('class', 'division_grid');
+
+ this.updateSizes();
+
+ if (initOps.workspace) {
+ this.handle = new UiElementHandle(this, this.node, initOps.workspace, ['resize_width_height', 'adjust_settings']);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+
+ updateSizes() {
+ this.rect.setAttributeNS(null, 'width', this.width + '');
+ this.rect.setAttributeNS(null, 'height', this.height + '');
+
+ this.block.blockData.dimensions = { width: this.width, height: this.height };
+
+ this._updateInternalElementSizes();
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+
+ _updateInternalElementSizes() {
+ this.rect.setAttributeNS(null, 'width', this.width + '');
+ this.rect.setAttributeNS(null, 'height', this.height + '');
+
+ const textBox = this.placeholder.getBBox();
+ this.placeholder.setAttributeNS(null, 'x', (this.width - textBox.width) / 2 + '');
+ this.placeholder.setAttributeNS(null, 'y', this.height / 2 + textBox.height / 2 + '');
+
+ this.block.blockData.dimensions = { width: this.width, height: this.height };
+ }
+
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+
+ // Resizeable
+ resize(dim: { x?: number, y?: number, width: number; height: number; }) {
+ // Check that what the minimum available size is
+ const fullContents = this.block.recursiveGetAllContents();
+
+ const inflexibleArea = combinedArea(
+ fullContents
+ .filter(b => b instanceof UiFlowBlock)
+ .filter(b => (!(b instanceof ContainerFlowBlock)) || (b.isAutoresizable()))
+ .map((b: UiFlowBlock) => {
+ const area = b.getBodyArea();
+
+ if (b.isAutoresizable()) {
+ const min = b.getMinSize();
+ area.width = min.width;
+ area.height = min.height;
+ }
+ return area;
+ }));
+ const wasPos = this.block.getOffset();
+
+ const mov = {
+ x: wasPos.x - (inflexibleArea.x - SEPARATION),
+ y: wasPos.y - (inflexibleArea.y - SEPARATION),
+ };
+
+ (this.block as ContainerFlowBlock).moveContents(mov);
+
+ const minWidth = Math.max(
+ MIN_WIDTH,
+ inflexibleArea.width === 0 ? 0 : inflexibleArea.width + SEPARATION * 2,
+ );
+
+ const minHeight = Math.max(
+ MIN_HEIGHT,
+ inflexibleArea.height === 0 ? 0 : inflexibleArea.height + SEPARATION * 2,
+ );
+
+ this.width = Math.max(minWidth, dim.width);
+ this.height = Math.max(minHeight, dim.height);
+
+ this.updateSizes();
+
+ (this.block as ContainerFlowBlock).update();
+ this.handle.update();
+
+ for (const content of this._contents) {
+ if (content instanceof ContainerFlowBlock) {
+ content.updateContainer(this.block);
+ }
+ else if ((content instanceof UiFlowBlock) && content.isAutoresizable()) {
+ content.updateContainer(this.block);
+ }
+ }
+ }
+
+ // UiFlowBlock
+ onClick() {
+ }
+
+ isAutoresizable(): this is Autoresizable {
+ return true;
+ }
+
+ getMinSize() {
+ if (this._contents.length === 0) {
+ return { width: MIN_WIDTH, height: MIN_HEIGHT };
+ }
+ const area = this.rect.getBBox();
+
+ return {
+ width: Math.max(area.width, MIN_WIDTH),
+ height: Math.max(area.height, MIN_HEIGHT),
+ }
+ }
+
+ doesTakeAllHorizontal() {
+ return false;
+ }
+
+ doesTakeAllVertical() {
+ return false;
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return false;
+ }
+
+ dispose() {}
+
+ onInputUpdated(connectedBlock: FlowBlock, inputIndex: number) {}
+
+ onConnectionLost(portIndex: number) {}
+
+ onConnectionValueUpdate(_inputIndex: number, value: string) {}
+
+ // Container element
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ onContentUpdate(contents: FlowBlock[]) {
+ this._contents = contents;
+ }
+
+ updateContainer(container: UiFlowBlock | null) {
+ if (container instanceof ContainerFlowBlock) {
+ this.container = container;
+ }
+ }
+
+ repositionContents(): void {
+ const allContents = this.block.recursiveGetAllContents();
+ const { tree: cutTree, toCenter: toCenter} = PositionResponsiveContents(this, this._contents, allContents, this.getBodyArea());
+
+ if (!cutTree) {
+ // No contents
+ const minArea = this.getMinSize();
+ const off = this.block.getOffset();
+
+ this.resize({
+ x: off.x,
+ y: off.y,
+ width: minArea.width,
+ height: minArea.height,
+ });
+
+ return;
+ }
+
+ const contentDict = listToDict(
+ allContents.filter(x => x instanceof UiFlowBlock) as UiFlowBlock[],
+ c => c.id);
+
+ const elems = getElementsInGroup(cutTree)
+ .map(id => contentDict[id])
+ .filter(x => x.isHorizontallyStackable());
+
+ const newArea = getRect(elems);
+
+ this.resize(manipulableAreaToArea2D(newArea));
+ CenterElements(toCenter);
+ }
+
+ dropOnEndMove() {
+ return { x: 0, y: 0 };
+ }
+
+ update() {
+ this.onContentUpdate(this._contents);
+ }
+
+ updateOptions() {
+ this._applyConfiguration(this.block.blockData.settings || {});
+ }
+
+ // Configurable
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return { target: { link: true } };
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ return Object.assign({}, this.block.blockData.settings || {});
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions): void {
+ const settingsStorage = Object.assign({}, this.block.blockData.settings || {});
+
+ if (settings.target) {
+ if (!settingsStorage.target) {
+ settingsStorage.target = {};
+ }
+ if (settings.target.link) {
+ settingsStorage.target.link = { value: settings.target.link.value };
+ }
+ if (settings.target.openInTab) {
+ settingsStorage.target.openInTab = { value: settings.target.openInTab.value };
+ }
+ }
+
+ this.block.blockData.settings = settingsStorage;
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+
+ this.block.notifyOptionsChange();
+ }
+
+ // Compilation
+ treeWith(content: CutTree): CutTree {
+ const tree: ContainerElementRepr = {
+ container_type: 'link_area',
+ id: this.block.id,
+ content: content,
+ settings: this.block.blockData.settings || {},
+ };
+
+ const settings = this.block.blockData.settings;
+ if (settings) {
+ tree.settings = settings;
+ }
+
+ return tree;
+ }
+}
+
+export const LinkAreaGenerateTree: GenTreeProc = (handler: UiFlowBlockHandler, blocks: FlowBlock[]) => {
+ const content = ResponsivePageGenerateTree(handler, blocks);
+
+ // Finally generate the tree
+ return (handler as LinkArea).treeWith(content);
+};
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/positioning.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/positioning.ts
new file mode 100644
index 00000000..81053b73
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/positioning.ts
@@ -0,0 +1,395 @@
+import { Area2D, FlowBlock, Position2D } from "../../flow_block";
+import { UiFlowBlock, UiFlowBlockHandler } from "../ui_flow_block";
+import { cleanestTree, getElementsInGroup, getRect, getShallowElementsInGroup, safeReduceTree } from "./responsive_page";
+import { CutNode, CutTree, ContainerElementRepr, CutType, DEFAULT_CUT_TYPE } from "./ui_tree_repr";
+import { manipulableAreaToArea2D } from "./utils";
+import { ContainerFlowBlock } from "../container_flow_block";
+
+export const SEPARATION = 25;
+
+interface ResponsivePositionToCenter {
+ cut_type: CutType,
+ elements: UiFlowBlock[],
+ treeElements: UiFlowBlock[],
+};
+
+// Positioning
+export function PositionResponsiveContents(handler: UiFlowBlockHandler,
+ blocks: FlowBlock[],
+ allBlocks: FlowBlock[],
+ offset: Position2D,
+ ): {tree: CutTree | null, toCenter: ResponsivePositionToCenter[]} {
+ // Format in a grid-like
+ const uiPos = (blocks
+ .filter(b => (b instanceof UiFlowBlock))
+ .map((b: UiFlowBlock, i) => {
+ const area = b.getBodyArea()
+ if (b.isAutoresizable()) {
+ const min = b.getMinSize();
+ area.width = min.width;
+ area.height = min.height;
+ }
+
+ return {i, a: area, b: (b as UiFlowBlock)};
+ }));
+
+ if (uiPos.length === 0){
+ return { tree: null, toCenter: [] };
+ }
+
+ const tree = safeReduceTree(cleanestTree(uiPos, uiPos.map(({b: block}) => block)));
+
+ const uiBlocks = allBlocks.filter(b => b instanceof UiFlowBlock) as UiFlowBlock[];
+ const blockMap: {[key: string]: UiFlowBlock} = {};
+ for (const block of uiBlocks) {
+ blockMap[block.id] = block;
+ }
+
+ let positioningTree = tree;
+
+ // Force to at least center vertically
+ if (!(positioningTree as CutNode).cut_type) {
+ positioningTree = { cut_type: DEFAULT_CUT_TYPE, groups: [ positioningTree ] };
+ }
+
+ const toCenter = PositionTreeContentsFromTree(positioningTree, blockMap, offset);
+
+ return { tree, toCenter };
+}
+
+function PositionTreeContentsFromTree(tree: CutTree, blocks: {[key: string]: UiFlowBlock}, offset: Position2D, nonTopLevel?: boolean): ResponsivePositionToCenter[] {
+ if (!(tree as CutNode).cut_type) {
+ return [];
+ }
+
+ if ((tree as CutNode).block_id) {
+ const id = (tree as CutNode).block_id;
+ const block = blocks[id];
+
+ if (block instanceof ContainerFlowBlock) {
+ // The block should have repositioned itself, no need to call it again
+ }
+ else {
+ throw Error("Cut with blockId that didn't correspond to a ContainerFlowBlock");
+ }
+ return [];
+ }
+
+ let positions: ResponsivePositionToCenter[] = [];
+
+ const cTree = tree as CutNode;
+ const toCenter: UiFlowBlock[] = [];
+
+ // First position subtrees
+ const subTreeOffset: Position2D = {
+ x: offset.x + (nonTopLevel ? 0 : SEPARATION),
+ y: offset.y + (nonTopLevel ? 0 : SEPARATION),
+ };
+
+ for (const group of cTree.groups) {
+ let area: Area2D;
+
+ if (((group as CutNode).block_id) || ((group as ContainerElementRepr).container_type) ) {
+ // Treat as a single block
+
+ const id = (group as CutNode).block_id ? (group as CutNode).block_id : (group as ContainerElementRepr).id ;
+ const block = blocks[id];
+ area = block.getBodyArea();
+
+ const mov = {
+ x: subTreeOffset.x - area.x,
+ y: subTreeOffset.y - area.y,
+ };
+
+ if (block.isAutoresizable()) {
+ // Don't push away from borders
+ if (cTree.cut_type === 'vbox') {
+ if (block.doesTakeAllHorizontal()) {
+ mov.x = 0;
+ }
+ }
+ }
+
+ block.moveBy(mov);
+ toCenter.push(block);
+
+ area = block.getBodyArea();
+ if (block.isAutoresizable) {
+ const minSize = block.getMinSize();
+ area.height = Math.max(minSize.height, area.height);
+ area.width = Math.max(minSize.width, area.width);
+ }
+ }
+ else {
+ // Treat as a group
+ const subOffset = { x: subTreeOffset.x, y: subTreeOffset.y };
+
+ positions = positions.concat(PositionTreeContentsFromTree(group, blocks, subOffset, true));
+
+ const contents = getElementsInGroup(group);
+ area = manipulableAreaToArea2D(getRect(contents.map(id => blocks[id])));
+ const mov = {
+ x: subTreeOffset.x - area.x,
+ y: subTreeOffset.y - area.y,
+ };
+
+ const elementsToMove = getShallowElementsInGroup(group).map(id => blocks[id]);
+
+ for (const block of elementsToMove) {
+
+ if (block.isAutoresizable()) {
+
+ // This really means "is this considered on PositionTreeContentsFromTree()"
+ if (block instanceof ContainerFlowBlock) {
+ continue;
+ }
+
+ // Don't push away from borders
+ if (cTree.cut_type === 'vbox') {
+ mov.x -= SEPARATION;
+ }
+ else if (cTree.cut_type === 'hbox') {
+ mov.y -= SEPARATION;
+ }
+ }
+ else {
+ toCenter.push(block);
+ }
+
+ block.moveBy(mov);
+ }
+
+ area = manipulableAreaToArea2D(getRect(contents.map(id => blocks[id])));
+ }
+
+ if (cTree.cut_type === 'vbox') {
+ subTreeOffset.y += area.height + SEPARATION;
+ }
+ else if (cTree.cut_type === 'hbox') {
+ subTreeOffset.x += area.width + SEPARATION;
+ }
+ }
+
+ positions.push({ cut_type: cTree.cut_type, elements: toCenter, treeElements: getElementsInGroup(cTree).map(id => blocks[id]) });
+ return positions;
+
+}
+
+export function PositionHorizontalContents(handler: UiFlowBlockHandler, blocks: FlowBlock[], area: Area2D): { width: number, height: number } {
+ const blockAreas: [UiFlowBlock, Area2D, Area2D][] = (
+ blocks
+ .filter(b => b instanceof UiFlowBlock)
+ .map((b: UiFlowBlock) => {
+ const area = b.getBodyArea()
+ let minArea: Area2D;
+ if (b.isAutoresizable()) {
+ const min = b.getMinSize();
+ area.width = min.width;
+ area.height = min.height;
+ minArea = Object.assign({}, area);
+
+ if (!b.doesTakeAllVertical()) {
+ area.height += SEPARATION * 2;
+ }
+ }
+ else {
+ minArea = Object.assign({}, area);
+ area.height += SEPARATION * 2;
+ }
+
+ return [b, area, minArea];
+ }));
+
+ const blockHeights = blockAreas.map(([_, a]) => a.height);
+ const maxHeight = Math.max(...blockHeights);
+
+ const blockWidths = blockAreas.map(([_, a]) => a.width);
+ const reqBlockWidth = blockWidths.reduce((a,b) => a + b, 0);
+
+ const sumPaddings = SEPARATION * (blocks.length + 1);
+ const reqWidth = reqBlockWidth + sumPaddings;
+ const height = maxHeight;
+
+ const separation = SEPARATION;
+
+ blockAreas.sort(([_11, a1, _13], [_21, a2, _23]) => a1.x - a2.x);
+ let xpos = separation;
+ for (const [block, blockArea, minArea] of blockAreas) {
+ const x = xpos;
+ const y = block.isAutoresizable() && block.doesTakeAllVertical() ? 0 : (height - minArea.height) / 2;
+
+ const absX = area.x + x;
+ const absY = area.y + y;
+
+ block.moveBy({
+ x: absX - blockArea.x,
+ y: absY - blockArea.y,
+ });
+
+ const areaAfterMove = block.getBodyArea()
+ if (block.isAutoresizable()) {
+ const min = block.getMinSize();
+ areaAfterMove.width = min.width;
+ areaAfterMove.height = min.height;
+ }
+
+ xpos += areaAfterMove.width + separation;
+ }
+
+ return { width: xpos, height };
+}
+
+export function PositionVerticalContents(handler: UiFlowBlockHandler, blocks: FlowBlock[], area: Area2D): { width: number, height: number } {
+ const blockAreas: [UiFlowBlock, Area2D, Area2D][] = (
+ blocks
+ .filter(b => b instanceof UiFlowBlock)
+ .map((b: UiFlowBlock) => {
+ const area = b.getBodyArea();
+ let minArea: Area2D;
+ if (b.isAutoresizable()) {
+ const min = b.getMinSize();
+ area.width = min.width;
+ area.height = min.height;
+ minArea = Object.assign({}, area);
+
+ if (!b.doesTakeAllHorizontal()) {
+ area.width += SEPARATION * 2;
+ }
+ }
+ else {
+ minArea = Object.assign({}, area);
+ area.width += SEPARATION * 2;
+ }
+
+ return [b, area, minArea];
+ }));
+
+ const blockWidths = blockAreas.map(([_, a]) => a.width);
+ const maxWidth = Math.max(...blockWidths);
+
+ const blockHeights = blockAreas.map(([_, a]) => a.height);
+ const reqBlockHeight = blockHeights.reduce((a,b) => a + b, 0);
+
+ const sumPaddings = SEPARATION * (blocks.length + 1);
+ const reqHeight = reqBlockHeight + sumPaddings;
+ const width = maxWidth;
+
+ const separation = SEPARATION;
+
+ blockAreas.sort(([_11, a1, _13], [_21, a2, _23]) => a1.y - a2.y);
+ let ypos = separation;
+ for (const [block, blockArea, minArea] of blockAreas) {
+ const y = ypos;
+ const x = block.isAutoresizable() && block.doesTakeAllHorizontal() ? 0 : (width - minArea.width) / 2;
+
+ const absX = area.x + x;
+ const absY = area.y + y;
+
+ block.moveBy({
+ x: absX - blockArea.x,
+ y: absY - blockArea.y,
+ });
+
+ const areaAfterMove = block.getBodyArea()
+ if (block.isAutoresizable()) {
+ const min = block.getMinSize();
+ areaAfterMove.width = min.width;
+ areaAfterMove.height = min.height;
+ }
+
+ ypos += areaAfterMove.height + separation;
+ }
+
+ return { width, height: ypos };
+}
+
+// Sizing
+export function GetMinSizeHorizontal(blocks: FlowBlock[]): { width: number, height: number } {
+ const blockAreas: [UiFlowBlock, Area2D][] = (
+ blocks
+ .filter(b => b instanceof UiFlowBlock)
+ .map((b: UiFlowBlock) => {
+ const area = b.getBodyArea()
+ if (b.isAutoresizable()) {
+ const min = b.getMinSize();
+ area.width = min.width;
+ area.height = min.height;
+ }
+ else {
+ area.height += SEPARATION * 2;
+ }
+
+ return [b, area];
+ }));
+
+ const blockHeights = blockAreas.map(([_, a]) => a.height);
+ const maxHeight = Math.max(...blockHeights);
+
+ const blockWidths = blockAreas.map(([_, a]) => a.width);
+ const reqBlockWidth = blockWidths.reduce((a,b) => a + b, 0);
+
+ const sumPaddings = SEPARATION * (blocks.length + 1);
+ const reqWidth = reqBlockWidth + sumPaddings;
+
+ return { width: reqWidth, height: maxHeight };
+}
+
+export function GetMinSizeVertical(blocks: FlowBlock[]): { width: number, height: number } {
+ const blockAreas: [UiFlowBlock, Area2D][] = (
+ blocks
+ .filter(b => b instanceof UiFlowBlock)
+ .map((b: UiFlowBlock) => {
+ const area = b.getBodyArea()
+ if (b.isAutoresizable()) {
+ const min = b.getMinSize();
+ area.width = min.width;
+ area.height = min.height;
+ }
+ else {
+ area.width += SEPARATION * 2;
+ }
+
+ return [b, area];
+ }));
+
+ const blockWidths = blockAreas.map(([_, a]) => a.width);
+ const blockHeights = blockAreas.map(([_, a]) => a.height);
+
+ const maxWidth = Math.max(...blockWidths);
+ const reqBlockHeight = blockHeights.reduce((a,b) => a + b, 0);
+
+ const sumPaddings = SEPARATION * (blocks.length + 1);
+ const reqHeight = reqBlockHeight + sumPaddings;
+
+ return { width: maxWidth, height: reqHeight };
+}
+
+// Centering
+export function CenterElements(groups: ResponsivePositionToCenter[]) {
+ for (const group of groups) {
+ const fullArea = manipulableAreaToArea2D(getRect(group.treeElements));
+ for (const elem of group.elements) {
+ const eArea = elem.getBodyArea();
+
+
+ if (elem.isAutoresizable()) {
+ const minArea = elem.getMinSize();
+
+ eArea.width = minArea.width;
+ eArea.height = minArea.height;
+ }
+
+ const mov = { x: 0, y: 0 };
+ if (group.cut_type === 'vbox') {
+ const xPos = fullArea.x + ((fullArea.width - eArea.width) / 2)
+ mov.x = xPos - eArea.x;
+ }
+ else if (group.cut_type === 'hbox') {
+ const yPos = fullArea.y + ((fullArea.height - eArea.height) / 2)
+ mov.y = yPos - eArea.y;
+ }
+
+ elem.moveBy(mov);
+ }
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/responsive_page.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/responsive_page.ts
new file mode 100644
index 00000000..e85ac167
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/responsive_page.ts
@@ -0,0 +1,805 @@
+import { Subscription } from "rxjs";
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { Area2D, FlowBlock, Position2D } from "../../flow_block";
+import { ContainerFlowBlock, ContainerFlowBlockBuilder, ContainerFlowBlockHandler, GenTreeProc } from "../container_flow_block";
+import { TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilderInitOps, UiFlowBlockHandler } from "../ui_flow_block";
+import { HandleableElement, UiElementHandle } from "./ui_element_handle";
+import { CutElement, CutNode, CutTree, CutType, UiElementRepr, ContainerElementRepr, DEFAULT_CUT_TYPE } from "./ui_tree_repr";
+import { combinedManipulableArea, getRefBox, listToDict, manipulableAreaToArea2D } from "./utils";
+import { PositionResponsiveContents, SEPARATION, CenterElements } from "./positioning";
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+const Title = "Responsive page";
+const TITLE_PADDING = 5;
+
+export const MIN_WIDTH = 200;
+export const MIN_HEIGHT = 400;
+
+export const ResponsivePageBuilder : ContainerFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: ContainerFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+ ) => {
+ const element = new ResponsivePage(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class ResponsivePage implements ContainerFlowBlockHandler, HandleableElement, TextEditable {
+ subscription: Subscription;
+ textBox: SVGTextElement;
+ handle: UiElementHandle | null = null;
+ node: SVGGElement;
+ textDim: { width: number; height: number; };
+ rect: SVGRectElement;
+ rectShadow: SVGRectElement;
+ grid: SVGGElement;
+ width: number;
+ height: number;
+ titleBox: SVGRectElement;
+ title: string;
+ contents: FlowBlock[] = [];
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ public block: ContainerFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ const refBox = getRefBox(canvas);
+
+ this.titleBox = document.createElementNS(SvgNS, 'rect');
+ this.node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.rectShadow = document.createElementNS(SvgNS, 'rect');
+
+ group.setAttribute('class', 'flow_node ui_node container_node responsive_page');
+
+ this.grid = document.createElementNS(SvgNS, 'g');
+
+ this.textBox = document.createElementNS(SvgNS, 'text');
+ this.textBox.setAttribute('class', 'output_text');
+ this.textBox.setAttributeNS(null,'textlength', '100%');
+
+ this.textBox.textContent = this.title = block.blockData.textContent || Title;
+
+ this.node.appendChild(this.rectShadow);
+ this.node.appendChild(this.rect);
+ this.node.appendChild(this.grid);
+ this.node.appendChild(this.titleBox);
+
+ this.node.appendChild(this.textBox);
+ group.appendChild(this.node);
+
+ const text_width = this.textBox.getBoundingClientRect().width;
+ const text_height = this.textBox.getBoundingClientRect().height;
+ this.textDim = { width: text_width, height: text_height };
+
+ const bdims = block.blockData.dimensions;
+ this.width = bdims ? bdims.width : text_width * 4;
+ this.height = bdims ? bdims.height : refBox.height * 30;
+
+ this.textBox.setAttributeNS(null, 'y', this.height/2 - text_height / 2 + "");
+ this.textBox.setAttributeNS(null, 'x', (this.width - text_width)/2 + "");
+
+ this.titleBox.setAttributeNS(null, 'class', 'titlebox');
+ this.titleBox.setAttributeNS(null, 'x', "0");
+ this.titleBox.setAttributeNS(null, 'y', "0");
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.rectShadow.setAttributeNS(null, 'class', "body_shadow");
+ this.rectShadow.setAttributeNS(null, 'x', "0");
+ this.rectShadow.setAttributeNS(null, 'y', "0");
+
+ this.grid.setAttribute('class', 'division_grid');
+
+ this.updateSizes();
+
+ if (initOps.workspace) {
+ this.handle = new UiElementHandle(this, this.node, initOps.workspace, []);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+ // Resizeable
+ resize(dim: Area2D) {
+ const baseWidth = Math.max(MIN_WIDTH, dim.width);
+ const baseHeight = Math.max(MIN_HEIGHT, dim.height);
+
+ const off = this.block.getOffset();
+ let right = off.x + baseWidth;
+ let bottom = off.y + baseHeight;
+
+ const contents = this.block.recursiveGetAllContents();
+ for (const c of contents) {
+
+ if (! (c instanceof UiFlowBlock)) {
+ continue;
+ }
+
+ const bArea = c.getBodyArea();
+
+ let separationX = SEPARATION;
+ const separationY = SEPARATION;
+
+ if (c.isAutoresizable()) {
+ const minArea = c.getMinSize();
+
+ bArea.width = minArea.width;
+ bArea.height = minArea.height;
+
+ if (!c.isHorizontallyStackable()) {
+ separationX = 0;
+ bArea.x = off.x;
+ }
+ }
+
+ const bRight = bArea.x + bArea.width + separationX;
+ const bBottom = bArea.y + bArea.height + separationY;
+
+ if (bRight > right) {
+ right = bRight;
+ }
+ if (bBottom > bottom) {
+ bottom = bBottom;
+ }
+ }
+
+ const newWidth = (right - off.x);
+ const newHeight = (bottom - off.y);
+
+ const diffWidth = this.width - newWidth;
+ const diffHeight = this.height - newHeight;
+
+ this.width = newWidth;
+ this.height = newHeight;
+
+ this.updateSizes();
+
+ (this.block as ContainerFlowBlock).update();
+ this.handle.update();
+
+ for (const content of this.contents) {
+ if (content instanceof ContainerFlowBlock) {
+ content.updateContainer(this.block);
+ }
+ else if ((content instanceof UiFlowBlock) && content.isAutoresizable()) {
+ content.updateContainer(this.block);
+ }
+ }
+ }
+
+ updateSizes() {
+ const text_width = this.textBox.getBoundingClientRect().width;
+ const text_height = this.textBox.getBoundingClientRect().height;
+ this.textDim = { width: text_width, height: text_height };
+
+ const titleHeight = this.textDim.height + TITLE_PADDING * 2;
+ this.titleBox.setAttributeNS(null, 'width', this.width + '');
+ this.titleBox.setAttributeNS(null, 'height', titleHeight + "")
+
+ this.rect.setAttributeNS(null, 'width', this.width + '');
+ this.rect.setAttributeNS(null, 'height', this.height + '');
+
+ this.rectShadow.setAttributeNS(null, 'width', this.width + '');
+ this.rectShadow.setAttributeNS(null, 'height', this.height + '');
+
+ this.textBox.setAttributeNS(null, 'x', (this.width - this.textDim.width)/2 + "");
+ this.textBox.setAttributeNS(null, 'y', this.textDim.height + "");
+
+ this.block.blockData.dimensions = { width: this.width, height: this.height };
+ }
+
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ // UiFlowBlock
+ onClick() {
+ this.block.startEditing();
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ isTextEditable(): this is TextEditable {
+ return true;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return true;
+ }
+
+
+ get isStaticText(): boolean {
+ return true;
+ }
+
+ get editableTextName(): string {
+ return 'title';
+ }
+
+ public get text(): string {
+ return this.title;
+ }
+
+ public set text(val: string) {
+ this.textBox.textContent = this.block.blockData.textContent = this.title = val;
+ this.updateSizes();
+ }
+
+ // Text edition area
+ getArea(): Area2D {
+ return this.titleBox.getBBox();
+ }
+
+ dispose() {}
+
+ onInputUpdated(connectedBlock: FlowBlock, inputIndex: number) {}
+
+ onConnectionLost(portIndex: number) {}
+
+ onConnectionValueUpdate(_inputIndex: number, value: string) {}
+
+ // Container element
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ onContentUpdate(contents: FlowBlock[]) {
+ // Obtain new distribution
+ this.contents = contents.concat([]);
+ }
+
+ _updateCutGrid() {
+ const uiContents = this.contents.filter(b => (b instanceof UiFlowBlock)) as UiFlowBlock[];
+
+ // Update grid
+ try {
+ const tree = ResponsivePageGenerateTree(this, this.contents);
+
+ this.grid.innerHTML = ''; // Clear it
+ const cuts = performCuts(tree, uiContents, this.width, this.height, this.block.getOffset());
+
+ for (const cut of cuts) {
+ const path = document.createElementNS(SvgNS, 'path');
+ path.setAttribute('class', `grid-division ${cut.type}-cut`);
+ path.setAttributeNS(null, 'd', `M${ cut.from.x },${cut.from.y} L${cut.to.x},${cut.to.y}`);
+ this.grid.appendChild(path);
+ }
+ }
+ catch(err) {
+ console.error(err);
+ this.grid.innerHTML = ''; // Make sure it's clear
+ }
+ }
+
+ dropOnEndMove() {
+ this._updateCutGrid();
+
+ return {x: 0, y: 0};
+ }
+
+ updateContainer(container: UiFlowBlock) {
+ if (container !== null) {
+ throw new Error("A webpage cannot be put inside a container. Trying to put inside a " + container.options.block_id);
+ }
+ }
+
+ repositionContents(): void {
+ const area = this.getBodyArea();
+
+ const titleHeight = this.titleBox.getBBox().height;
+ area.y += titleHeight;
+
+ const allContents = this.block.recursiveGetAllContents();
+
+ const { tree: cutTree, toCenter: toCenter} = PositionResponsiveContents(this, this.contents, allContents, area);
+
+ if (!cutTree) {
+ // No contents, just set to min size
+
+ this.resize({ x: area.x, y: area.y, width: MIN_WIDTH, height: MIN_HEIGHT });
+
+ return;
+ }
+
+ const contentDict = listToDict(
+ allContents.filter(x => x instanceof UiFlowBlock) as UiFlowBlock[],
+ c => c.id);
+
+ const elems = getElementsInGroup(cutTree)
+ .map(id => contentDict[id])
+ .filter(b => !b.isAutoresizable());
+
+ const newArea = getRect(elems);
+
+ this.resize(manipulableAreaToArea2D(newArea));
+
+ CenterElements(toCenter);
+ }
+
+ get container(): ContainerFlowBlock {
+ return null;
+ }
+
+ update() {
+ this.onContentUpdate(this.contents);
+ }
+
+ updateOptions() {
+ this.textBox.textContent = this.title = this.block.blockData.textContent || Title;
+ this.updateSizes();
+ }
+}
+
+type GridCut = { from: Position2D, to: Position2D, type: 'vert' | 'horiz' };
+
+function performCuts(tree: CutTree, contents: UiFlowBlock[], width: number, height: number, offset: Position2D
+ ): GridCut[] {
+ const acc: GridCut[] = [];
+ let todo = [{ tree: tree, area: { x: 0, y: 0, width, height } }];
+
+ const blocks: {[key: string]: UiFlowBlock} = {};
+ for (const block of contents) {
+ blocks[block.id] = block;
+ if (block instanceof ContainerFlowBlock) {
+ for (const subBlock of block.recursiveGetAllContents()) {
+ if (subBlock instanceof UiFlowBlock) {
+ blocks[subBlock.id] = subBlock;
+ }
+ }
+ }
+ }
+
+ while (todo.length > 0) {
+ const cut = todo.pop();
+
+ if (!cut.tree) {
+ // Empty group
+ continue;
+ }
+
+ if ((cut.tree as UiElementRepr).widget_type) {
+ continue; // Single element, nothing to cut
+ }
+ else if ((cut.tree as ContainerElementRepr).container_type) {
+ continue; // Visual container, nothing to cut
+ }
+
+ const cTree = cut.tree as CutNode;
+ const elements = cTree.groups.map(g => getElementsInGroup(g));
+ const cGroups = elements.map((_value, idx) => idx).filter(idx => elements[idx].length > 0);
+
+ if (cTree.cut_type === 'vbox') {
+ // This cloning is probably not needed. Done now for simplicity.
+ const availArea = Object.assign({}, cut.area);
+
+ // Analyze as group pairs
+ for (let i = 1; i < cGroups.length; i++) {
+
+
+ const r1 = getRect(elements[cGroups[i - 1]].map(e => blocks[e]));
+ const r2 = getRect(elements[cGroups[i]].map(e => blocks[e]));
+
+ const cutYPos = (r1.bottom + (r2.top - r1.bottom) / 2) - offset.y;
+
+ acc.push({ from: { x: availArea.x, y: cutYPos }, to: { x: availArea.x + availArea.width, y: cutYPos }, type: 'vert' });
+
+ const topArea = Object.assign({}, availArea);
+ topArea.height = cutYPos - availArea.y;
+ todo.push({ tree: cTree.groups[cGroups[i - 1]], area: topArea });
+
+ availArea.y = cutYPos;
+ availArea.height -= topArea.height;
+
+ if ((i + 1) === cGroups.length) {
+ // Only recurse bottom on the latest group, to avoid duplicating
+ todo.push({ tree: cTree.groups[cGroups[i]], area: availArea });
+ }
+ }
+ }
+ else if (cTree.cut_type === 'hbox') {
+ // This cloning is probably not needed. Done now for simplicity.
+ const availArea = Object.assign({}, cut.area);
+
+ // Analyze as group pairs
+ for (let i = 1; i < cGroups.length; i++) {
+
+ const r1 = getRect(elements[cGroups[i - 1]].map(e => blocks[e]));
+ const r2 = getRect(elements[cGroups[i]].map(e => blocks[e]));
+
+ const cutXPos = (r1.right + (r2.left - r1.right) / 2) - offset.x;
+
+ acc.push({ from: { x: cutXPos, y: availArea.y }, to: { x: cutXPos, y: availArea.y + availArea.height }, type: 'horiz' });
+
+ const leftArea = Object.assign({}, availArea);
+ leftArea.width = cutXPos - availArea.x;
+ todo.push({ tree: cTree.groups[cGroups[i - 1]], area: leftArea });
+
+ availArea.x = cutXPos;
+ availArea.width -= leftArea.width;
+
+ if ((i + 1) === cGroups.length) {
+ // Only recurse right on the latest group, to avoid duplicating
+ todo.push({ tree: cTree.groups[cGroups[i]], area: availArea });
+ }
+ }
+ }
+ else {
+ throw new Error("Unknown cut type: " + cTree.cut_type);
+ }
+ }
+
+ return acc;
+}
+
+export function getElementsInGroup(tree: CutTree): string[] {
+ let acc = [];
+
+ const todo = [tree];
+ while (todo.length > 0) {
+ const cut = todo.pop();
+
+ if (!cut) {
+ continue;
+ }
+
+ if ((cut as UiElementRepr).widget_type) {
+ acc.push((cut as UiElementRepr).id);
+ }
+ else if ((cut as CutNode).groups) {
+ if ((cut as CutNode).block_id) {
+ acc.push((cut as CutNode).block_id);
+ }
+ for (const group of (cut as CutNode).groups) {
+ todo.push(group);
+ }
+ }
+ else if ((cut as ContainerElementRepr).container_type) {
+ if ((cut as ContainerElementRepr).id) {
+ acc.push((cut as ContainerElementRepr).id);
+ }
+
+ todo.push((cut as ContainerElementRepr).content);
+ }
+ else {
+ console.warn("Unexpected node:", cut);
+ throw Error("Unexpected node: " + cut);
+ }
+ }
+
+ return acc;
+}
+
+export function getShallowElementsInGroup(tree: CutTree): string[] {
+ let acc = [];
+
+ const todo = [tree];
+ while (todo.length > 0) {
+ const cut = todo.pop();
+
+ if (!cut) {
+ continue;
+ }
+
+ if ((cut as UiElementRepr).widget_type) {
+ acc.push((cut as UiElementRepr).id);
+ }
+ else if ((cut as CutNode).groups) {
+ if ((cut as CutNode).block_id) {
+ acc.push((cut as CutNode).block_id);
+ }
+ }
+ else if ((cut as ContainerElementRepr).container_type) {
+ acc.push((cut as ContainerElementRepr).id);
+ }
+ else {
+ console.warn("Unexpected node:", cut);
+ throw Error("Unexpected node: " + cut);
+ }
+ }
+
+ return acc;
+}
+
+export function getRect(blocks: UiFlowBlock[]) {
+ return combinedManipulableArea(blocks.map(b => b.getBodyArea()));
+}
+
+export const ResponsivePageGenerateTree: GenTreeProc = (handler: UiFlowBlockHandler, blocks: FlowBlock[]) => {
+ // Format in a grid-like
+ const uiPos = (blocks
+ .filter(b => (b instanceof UiFlowBlock))
+ .map((b, i) => {
+ return {i, a: b.getBodyArea(), b: (b as UiFlowBlock)};
+ }));
+
+ if (uiPos.length < 1) {
+ return null;
+ }
+ if (uiPos.length < 2) {
+ return uiPos[0].b.renderAsUiElement();
+ }
+
+ const tree = cleanestTree(uiPos, uiPos.map(({b: block}) => block));
+
+ return reduceTree(tree);
+}
+
+// These two "reduce" functions might be merged into a single one. It's just not
+// a priority right now, but it might be interesting to do it to check if it
+// results on simpler code.
+export function safeReduceTree(tree: CutTree) {
+ return _reduceTree(tree, true);
+}
+
+function reduceTree(tree: CutTree): CutTree {
+ return _reduceTree(tree, false);
+}
+
+function _reduceTree(tree: CutTree, safe: boolean): CutTree {
+ if (!((tree as CutNode).cut_type)) {
+ return tree;
+ }
+
+ const cNode = tree as CutNode;
+ const newGroups = _reduceGroups(cNode, safe);
+
+ const recasted: CutNode = { cut_type: cNode.cut_type, groups: newGroups };
+ if (cNode.settings) {
+ recasted.settings = cNode.settings;
+ }
+ if (cNode.block_id) {
+ recasted.block_id = cNode.block_id;
+ }
+ return recasted;
+}
+
+function _reduceGroups(cNode: CutNode, safe: boolean): CutTree[] {
+ let acc: CutTree[] = [];
+ const cType = cNode.cut_type;
+
+ const aux = (tree: CutTree) => {
+ if (!(tree as CutNode).cut_type) {
+ acc.push(tree);
+ return;
+ }
+
+ const cTree = tree as CutNode;
+
+ // Trees and nodes with settings are not merged to avoid losing "colors" in the process
+ let canMerge = ((!(cNode.settings && cNode.settings.bg))
+ && (cTree.cut_type === cType) // Cut type must be the same
+ && (!(cTree.settings && cTree.settings.bg)));
+
+ if (canMerge && safe) {
+ // Additional checks:
+ // - Neither of the blocks can have an ID
+ canMerge = canMerge && (!cNode.block_id) && (!cTree.block_id);
+ }
+
+ if (canMerge) {
+ for (const group of cTree.groups) {
+ aux(group);
+ }
+ }
+ else {
+ acc.push(_reduceTree(cTree, safe));
+ }
+ }
+
+ for (const group of cNode.groups) {
+ aux(group);
+ }
+
+ return acc;
+}
+
+// Recursively perform cleanestCut, until all elements are partitioned.
+export function cleanestTree(elems: CutElement[], blocks: UiFlowBlock[]): CutTree {
+ const topLevel: CutTree[] = [];
+
+ const todo = [ { container: topLevel, elems: elems } ]
+
+ let opNum = -1;
+
+ // Easy limit for test, if the function works correctly it should neverbe reached.
+ // Length * 2 should be enough, but some slack is allowed, given that this is only an intuition.
+ let maxOps = elems.length * 3;
+
+ while (todo.length > 0) {
+ // We process items in the same order as they are generated to respect the group order
+ const next = todo.shift();
+
+ opNum++;
+ if (opNum > maxOps) {
+ throw new Error('Infinite loop found tree-ifying.'
+ + ` Started with ${elems.length} elements, did ${opNum} cuts`
+ + ` and ${todo.length + 1} items remain.`);
+ }
+
+ let result : CutTree;
+ if (next.elems.length < 1) {
+ throw new Error(`Cannot build tree with < 1 element, found ${next.elems.length}`);
+ }
+ else if (next.elems.length === 1) {
+ const block = blocks[next.elems[0].i];
+ result = block.renderAsUiElement();
+ }
+ else {
+ const cut = cleanestCut(next.elems);
+
+ const resultGroups: CutTree[] = [];
+ result = { cut_type: cut.cutType, groups: resultGroups };
+ for (const g of cut.groups){
+ if (g.length > 0) {
+ todo.push({ container: resultGroups, elems: g });
+ }
+ }
+ }
+
+ next.container.push(result);
+ }
+
+ return topLevel[0];
+}
+
+// Perform a single cut that divides the elements on the place where there is more empty space.
+function cleanestCut(elems: CutElement[]): { cutType: CutType, groups: CutElement[][] } {
+ // Sort horizantally and vertically
+ const horiz = elems.map(e => {
+ if (e.b.isHorizontallyStackable()) {
+ return e;
+ }
+ else {
+ return {
+ b: e.b,
+ i: e.i,
+ a: {
+ y: e.a.y,
+ height: e.a.height,
+ // Don't stack horizontally
+ x: -Infinity,
+ width: Infinity,
+ }
+ }
+ }
+ });
+ horiz.sort((a, b) => a.a.x - b.a.x );
+
+ const vert = elems.concat([]);
+ vert.sort((a, b) => a.a.y - b.a.y );
+
+ // Measure horizontal spaces
+ const horizSpaces: [number, number, CutElement][] = [];
+ let endX = null;
+ for (let idx = 0; idx < horiz.length; idx++) {
+ const e = horiz[idx];
+
+ if (endX === null) {
+ endX = e.a.x + e.a.width;
+ horizSpaces.push([ -Infinity, idx, e ]);
+ continue;
+ }
+
+ const diff = e.a.x - endX;
+ endX = e.a.x + e.a.width;
+
+ horizSpaces.push([ diff, idx, e ]);
+ }
+
+ horizSpaces.sort(([a, aIdx], [b, bIdx]) => {
+ if (b != a) {
+ return b - a
+ }
+ else {
+ // If the biggest gap cannot be found, avoid using the first gap
+ // (between nothing and the first element) as cut point.
+ // To do this, for gaps with the same size, put the ones with higher "index" first.
+ return bIdx - aIdx;
+ }
+ })
+
+ // Measure vertical spaces
+ const vertSpaces: [number, number, CutElement][] = [];
+ let endY = null;
+ for (let idx = 0; idx < vert.length; idx++) {
+ const e = vert[idx];
+
+ if (endY === null) {
+ endY = e.a.y + e.a.height;
+ vertSpaces.push([ -Infinity, idx, e ]);
+ continue;
+ }
+
+ const diff = e.a.y - endY;
+ endY = e.a.y + e.a.height;
+
+ vertSpaces.push([ diff, idx, e ]);
+ }
+
+ vertSpaces.sort(([a, aIdx], [b, bIdx]) => {
+ if (b != a) {
+ return b - a
+ }
+ else {
+ // If the biggest gap cannot be found, avoid using the first gap
+ // (between nothing and the first element) as cut point.
+ // To do this, for gaps with the same size, put the ones with higher "index" first.
+ return bIdx - aIdx;
+ }
+ })
+
+ // Find how to cut, horizontally or vertically
+ let cutType : CutType = DEFAULT_CUT_TYPE;
+ if (horizSpaces.length < 1) {
+ if (vertSpaces.length < 1) {
+ cutType = DEFAULT_CUT_TYPE;
+ }
+ else {
+ cutType = 'vbox';
+ }
+ }
+ else if (vertSpaces.length < 1) {
+ cutType = 'hbox';
+ }
+ else {
+ const maxHoriz = horizSpaces[0][0];
+ const maxVert = vertSpaces[0][0];
+
+ if (maxHoriz > maxVert) {
+ cutType = 'hbox';
+ }
+ else {
+ cutType = 'vbox';
+ }
+ }
+
+ // Perform the cut on the index with the most space
+ let before: CutElement[], after:CutElement[];
+ if (cutType === 'hbox') {
+ const cutIdx = horizSpaces[0][1];
+ before = horiz.slice(0, cutIdx);
+ after = horiz.slice(cutIdx);
+ }
+ else {
+ const cutIdx = vertSpaces[0][1];
+ before = vert.slice(0, cutIdx);
+ after = vert.slice(cutIdx);
+ }
+
+ if((before.length === 0) || (after.length === 0)) {
+ throw Error(`Splitting with no elements on one side (${before.length} -split- ${after.length})`);
+ }
+
+ return { cutType: cutType, groups: [before, after] };
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/simple_button.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/simple_button.ts
new file mode 100644
index 00000000..3f5e001d
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/simple_button.ts
@@ -0,0 +1,132 @@
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { UiFlowBlock, UiFlowBlockHandler, UiFlowBlockBuilder, TextEditable, TextReadable } from "../ui_flow_block";
+import { FlowBlock, Area2D } from "../../flow_block";
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+const DefaultContent = "Button";
+
+export const SimpleButtonBuilder: UiFlowBlockBuilder = (canvas: SVGElement, group: SVGElement, block: UiFlowBlock, service: UiSignalService) => {
+ return new SimpleButton(canvas, group, block, service);
+}
+
+class SimpleButton implements UiFlowBlockHandler, TextEditable {
+ private textBox: SVGTextElement;
+ private textValue: string;
+ private rect: SVGRectElement;
+ private rectShadow: SVGRectElement;
+ readonly MinWidth = 120;
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ private block: UiFlowBlock,
+ private service: UiSignalService) {
+ const node = document.createElementNS(SvgNS, 'a');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.rectShadow = document.createElementNS(SvgNS, 'rect');
+ const contentsGroup = document.createElementNS(SvgNS, 'g');
+
+
+ group.setAttribute('class', 'flow_node ui_node button_node');
+
+ this.textBox = document.createElementNS(SvgNS, 'text');
+ this.textBox.setAttribute('class', 'button_text');
+ this.textBox.setAttributeNS(null, 'textlength', '100%');
+
+ this.textValue = this.textBox.textContent = block.blockData.textContent || DefaultContent;
+ this.block.blockData.textContent = this.textValue;
+
+ contentsGroup.appendChild(this.textBox);
+ node.appendChild(this.rectShadow);
+ node.appendChild(this.rect);
+ node.appendChild(this.textBox);
+ group.appendChild(node);
+
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+ this.rect.setAttributeNS(null, 'rx', "5px"); // Like border-radius, in px
+
+ this.rectShadow.setAttributeNS(null, 'class', "body_shadow");
+ this.rectShadow.setAttributeNS(null, 'x', "0");
+ this.rectShadow.setAttributeNS(null, 'y', "0");
+ this.rectShadow.setAttributeNS(null, 'rx', "5px"); // Like border-radius, in px
+
+ this._updateSize();
+ }
+
+ getArea(): Area2D {
+ return this.rect.getBBox();
+ }
+
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+
+ onClick() {
+ // return this.service.sendBlockSignal(this.block.options.id, this.block.id);
+ }
+
+ isTextEditable(): this is TextEditable {
+ return true;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return true;
+ }
+
+ get isStaticText(): boolean {
+ return true;
+ }
+
+ get editableTextName(): string {
+ return 'label';
+ }
+
+ public get text(): string {
+ return this.textValue;
+ }
+
+ public set text(val: string) {
+ this.textBox.textContent = this.block.blockData.textContent = this.textValue = val;
+ this._updateSize();
+ }
+
+ updateOptions() {
+ this.textValue = this.textBox.textContent = this.block.blockData.textContent || DefaultContent;
+ this.block.blockData.textContent = this.textValue;
+ this._updateSize();
+ }
+
+ dispose() {}
+
+ onInputUpdated(block: FlowBlock, inputIndex: number) {}
+
+ onConnectionValueUpdate(inputIndex: number, value: string) {}
+
+ onConnectionLost(portIndex: number) {
+ this.onConnectionValueUpdate(portIndex, DefaultContent);
+ }
+
+ // Aux
+ _updateSize() {
+ const textArea = this.textBox.getBoundingClientRect();
+
+ const box_height = textArea.height * 3;
+ const box_width = Math.max(textArea.width + 50, this.MinWidth);
+
+ this.textBox.setAttributeNS(null, 'y', box_height/1.5 + "");
+ this.textBox.setAttributeNS(null, 'x', (box_width - textArea.width)/2 + "");
+
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+ this.rectShadow.setAttributeNS(null, 'height', box_height + "");
+ this.rectShadow.setAttributeNS(null, 'width', box_width + "");
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/simple_ui_card.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/simple_ui_card.ts
new file mode 100644
index 00000000..d7927860
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/simple_ui_card.ts
@@ -0,0 +1,395 @@
+import { Subscription } from "rxjs";
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { BlockAllowedConfigurations, BlockConfigurationOptions } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+import { Area2D, FlowBlock } from "../../flow_block";
+import { ContainerFlowBlock, ContainerFlowBlockBuilder, ContainerFlowBlockHandler, GenTreeProc } from "../container_flow_block";
+import { Autoresizable, TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilderInitOps, UiFlowBlockHandler } from "../ui_flow_block";
+import { PositionResponsiveContents, SEPARATION, CenterElements } from "./positioning";
+import { getElementsInGroup, getRect, ResponsivePageGenerateTree } from "./responsive_page";
+import { ConfigurableSettingsElement, HandleableElement, UiElementHandle } from "./ui_element_handle";
+import { ContainerElementRepr, CutTree } from "./ui_tree_repr";
+import { combinedArea, listToDict, manipulableAreaToArea2D } from "./utils";
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+const BLOCK_TYPE_ANNOTATION = 'Ui Card'
+const DEFAULT_COLOR = '';
+
+const MIN_WIDTH = 100;
+const MIN_HEIGHT = 100;
+
+export const SimpleUiCardBuilder: ContainerFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: ContainerFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const element = new SimpleUiCard(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class SimpleUiCard implements ContainerFlowBlockHandler, HandleableElement, Autoresizable, ConfigurableSettingsElement {
+ subscription: Subscription;
+ handle: UiElementHandle | null = null;
+ node: SVGGElement;
+ rect: SVGRectElement;
+ rectShadow: SVGRectElement;
+ grid: SVGGElement;
+ width: number;
+ height: number;
+ placeholder: SVGTextElement;
+ container: ContainerFlowBlock;
+ private _contents: FlowBlock[] = [];
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ public block: ContainerFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ this.node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ this.rectShadow = document.createElementNS(SvgNS, 'rect');
+ this.placeholder = document.createElementNS(SvgNS, 'text');
+
+ group.setAttribute('class', 'flow_node ui_node container_node card_node simple_card');
+
+ this.grid = document.createElementNS(SvgNS, 'g');
+
+ this.node.appendChild(this.rectShadow);
+ this.node.appendChild(this.rect);
+ this.node.appendChild(this.grid);
+ this.node.appendChild(this.placeholder);
+
+ group.appendChild(this.node);
+
+
+ this.placeholder.setAttributeNS(null, 'class', 'block_type_annotation');
+ this.placeholder.textContent = BLOCK_TYPE_ANNOTATION;
+
+ const text_width = this.placeholder.getBoundingClientRect().width;
+ const text_height = this.placeholder.getBoundingClientRect().height;
+ const textDim = { width: text_width, height: text_height };
+
+ const bdims = block.blockData.dimensions;
+ this.width = bdims ? bdims.width : textDim.width * 1.5;
+ this.height = bdims ? bdims.height : textDim.height * 2;
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+ this.rect.setAttributeNS(null, 'rx', "4");
+
+ this.rectShadow.setAttributeNS(null, 'class', "body_shadow");
+ this.rectShadow.setAttributeNS(null, 'x', "0");
+ this.rectShadow.setAttributeNS(null, 'y', "0");
+ this.rectShadow.setAttributeNS(null, 'rx', "4");
+
+ this.grid.setAttribute('class', 'division_grid');
+
+ this.updateStyle();
+ this.updateSizes();
+
+ if (initOps.workspace) {
+ this.handle = new UiElementHandle(this, this.node, initOps.workspace, ['resize_width_height', 'adjust_settings']);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+
+ updateSizes() {
+ this.rect.setAttributeNS(null, 'width', this.width + '');
+ this.rect.setAttributeNS(null, 'height', this.height + '');
+
+ this.rectShadow.setAttributeNS(null, 'width', this.width + '');
+ this.rectShadow.setAttributeNS(null, 'height', this.height + '');
+
+ this.block.blockData.dimensions = { width: this.width, height: this.height };
+
+ this._updateInternalElementSizes();
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+
+ _updateInternalElementSizes() {
+ this.rect.setAttributeNS(null, 'width', this.width + '');
+ this.rect.setAttributeNS(null, 'height', this.height + '');
+
+ this.rectShadow.setAttributeNS(null, 'width', this.width + '');
+ this.rectShadow.setAttributeNS(null, 'height', this.height + '');
+
+ const textBox = this.placeholder.getBBox();
+ this.placeholder.setAttributeNS(null, 'x', (this.width - textBox.width) / 2 + '');
+ this.placeholder.setAttributeNS(null, 'y', this.height / 2 + textBox.height / 2 + '');
+
+ this.block.blockData.dimensions = { width: this.width, height: this.height };
+ }
+
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+
+ // Resizeable
+ resize(dim: { x?: number, y?: number, width: number; height: number; }) {
+ // Check that what the minimum available size is
+ const fullContents = this.block.recursiveGetAllContents();
+
+ const inflexibleArea = combinedArea(
+ fullContents
+ .filter(b => b instanceof UiFlowBlock)
+ .filter(b => (!(b instanceof ContainerFlowBlock)) || (b.isAutoresizable()))
+ .map((b: UiFlowBlock) => {
+ const area = b.getBodyArea();
+
+ if (b.isAutoresizable()) {
+ const min = b.getMinSize();
+ area.width = min.width;
+ area.height = min.height;
+ }
+ return area;
+ }));
+
+ const wasPos = this.block.getOffset();
+
+ const mov = {
+ x: wasPos.x - (inflexibleArea.x - SEPARATION),
+ y: wasPos.y - (inflexibleArea.y - SEPARATION),
+ };
+
+ (this.block as ContainerFlowBlock).moveContents(mov);
+
+ const pos = this.block.getOffset();
+
+ const minWidth = Math.max(
+ MIN_WIDTH,
+ inflexibleArea.width === 0 ? 0 : inflexibleArea.width + SEPARATION * 2,
+ );
+
+ const minHeight = Math.max(
+ MIN_HEIGHT,
+ inflexibleArea.height === 0 ? 0 : inflexibleArea.height + SEPARATION * 2,
+ );
+
+ this.width = Math.max(minWidth, dim.width);
+ this.height = Math.max(minHeight, dim.height);
+
+ this.updateSizes();
+
+ (this.block as ContainerFlowBlock).update();
+ this.handle.update();
+
+ for (const content of this._contents) {
+ if (content instanceof ContainerFlowBlock) {
+ content.updateContainer(this.block);
+ }
+ else if ((content instanceof UiFlowBlock) && content.isAutoresizable()) {
+ content.updateContainer(this.block);
+ }
+ }
+ }
+
+ isAutoresizable(): this is Autoresizable {
+ return true;
+ }
+
+ doesTakeAllHorizontal() {
+ return false;
+ }
+
+ doesTakeAllVertical() {
+ return false;
+ }
+
+ getMinSize() {
+ if (this._contents.length === 0) {
+ return { width: MIN_WIDTH, height: MIN_HEIGHT };
+ }
+ const area = this.rect.getBBox();
+
+ return {
+ width: Math.max(area.width, MIN_WIDTH),
+ height: Math.max(area.height, MIN_HEIGHT),
+ }
+ }
+
+ // UiFlowBlock
+ onClick() {
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return false;
+ }
+
+ dispose() {}
+
+ onInputUpdated(connectedBlock: FlowBlock, inputIndex: number) {}
+
+ onConnectionLost(portIndex: number) {}
+
+ onConnectionValueUpdate(_inputIndex: number, value: string) {}
+
+ // Container element
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ onContentUpdate(contents: FlowBlock[]) {
+ this._contents = contents;
+ }
+
+ repositionContents(): void {
+ const allContents = this.block.recursiveGetAllContents();
+ const { tree: cutTree, toCenter: toCenter} = PositionResponsiveContents(this, this._contents, allContents, this.getBodyArea());
+
+ if (!cutTree) {
+ // No contents
+ const minArea = this.getMinSize();
+ const off = this.block.getOffset();
+
+ this.resize({
+ x: off.x,
+ y: off.y,
+ width: minArea.width,
+ height: minArea.height,
+ });
+
+ return;
+ }
+
+ const contentDict = listToDict(
+ allContents.filter(x => x instanceof UiFlowBlock) as UiFlowBlock[],
+ c => c.id);
+
+ const elems = getElementsInGroup(cutTree)
+ .map(id => contentDict[id])
+ .filter(x => x.isHorizontallyStackable());
+
+ const newArea = getRect(elems);
+
+ this.resize(manipulableAreaToArea2D(newArea));
+
+ CenterElements(toCenter);
+ }
+
+ updateContainer(container: UiFlowBlock | null) {
+ if (container instanceof ContainerFlowBlock) {
+ this.container = container;
+ }
+ }
+
+ dropOnEndMove() {
+ return { x: 0, y: 0 };
+ }
+
+ update() {
+ this.onContentUpdate(this._contents);
+ }
+
+ updateOptions() {
+ this._applyConfiguration(this.block.blockData.settings || {});
+ }
+
+ // Configurable
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return { background: {color: true, image: true} };
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ return Object.assign({}, this.block.blockData.settings || {});
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions): void {
+ if (settings.bg) {
+ this.block.blockData.settings = Object.assign(this.block.blockData.settings || {}, {bg: settings.bg});
+
+ this.updateStyle();
+ }
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+
+ this.block.notifyOptionsChange();
+ }
+
+ // Style management
+ updateStyle(){
+ const settings = this.block.blockData.settings;
+ if (!settings) {
+ return;
+ }
+ if (settings.bg) {
+ // Get color to apply
+ let color = DEFAULT_COLOR;
+ if (settings.bg.type === 'color') {
+ color = settings.bg.value;
+ }
+
+ // Apply it to the element's background
+ this.rect.style.fill = color;
+ }
+ }
+
+ // Compilation
+ treeWith(content: CutTree): CutTree {
+ const tree: ContainerElementRepr = {
+ container_type: 'simple_card',
+ id: this.block.id,
+ content: content,
+ settings: {},
+ };
+
+ const settings = this.block.blockData.settings;
+ if (settings) {
+ if (settings.bg && settings.bg.type === 'color') {
+ tree.settings.bg = settings.bg;
+ }
+ }
+
+ return tree;
+ }
+}
+
+export const SimpleUiCardGenerateTree: GenTreeProc = (handler: UiFlowBlockHandler, blocks: FlowBlock[]) => {
+ const content = ResponsivePageGenerateTree(handler, blocks);
+
+ // Finally generate the tree
+ return (handler as SimpleUiCard).treeWith(content);
+};
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/text_box.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/text_box.ts
new file mode 100644
index 00000000..9dfaa7ea
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/text_box.ts
@@ -0,0 +1,427 @@
+import { UiSignalService } from "../../../services/ui-signal.service";
+import { Area2D, FlowBlock } from "../../flow_block";
+import { TextEditable, TextReadable, UiFlowBlock, UiFlowBlockBuilder, UiFlowBlockBuilderInitOps, UiFlowBlockHandler, Autoresizable } from "../ui_flow_block";
+import { ConfigurableSettingsElement, HandleableElement, UiElementHandle } from "./ui_element_handle";
+import { BlockConfigurationOptions, BlockAllowedConfigurations, fontWeightToCss } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+import { ContainerFlowBlock } from "../container_flow_block";
+import { startOnElementEditor, FormattedTextTree, formattedTextTreeToDom } from "./utils";
+import { FlowWorkspace } from "../../flow_workspace";
+
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+export const MAX_WIDTH = 1024;
+
+const DefaultContent = { type: 'text', value: "Text box"};
+
+const DEFAULT_TEXT_COLOR = '#000';
+const DEFAULT_BACKGROUND_COLOR = '#eee';
+
+export const TextBoxBuilder: UiFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: UiFlowBlock,
+ service: UiSignalService,
+ initOps: UiFlowBlockBuilderInitOps,
+) => {
+ const element = new TextBox(canvas, group, block, service, initOps);
+ element.init();
+ return element;
+}
+
+class TextBox implements UiFlowBlockHandler, TextEditable, ConfigurableSettingsElement, HandleableElement {
+ private textBox: SVGForeignObjectElement;
+ private textValue: FormattedTextTree;
+ private rect: SVGRectElement;
+ readonly MinWidth = 200;
+ readonly MinHeight = 50;
+ private handle: UiElementHandle;
+ private _container: ContainerFlowBlock;
+ private contentBox: HTMLDivElement;
+ private editing = false;
+
+ private readonly workspace: FlowWorkspace;
+ private fullTextArea: Area2D;
+
+ constructor(canvas: SVGElement, group: SVGElement,
+ private block: UiFlowBlock,
+ private service: UiSignalService,
+ private initOps: UiFlowBlockBuilderInitOps) {
+
+ const node = document.createElementNS(SvgNS, 'g');
+ this.rect = document.createElementNS(SvgNS, 'rect');
+ const contentsGroup = document.createElementNS(SvgNS, 'g');
+
+ group.setAttribute('class', 'flow_node ui_node text_box');
+
+ this.textBox = document.createElementNS(SvgNS, 'foreignObject');
+ this.textBox.setAttribute('class', 'text');
+
+ if ((block.blockData.textContent) && !(block.blockData.content)) {
+ block.blockData.content = [{ type: 'text', value: block.blockData.textContent }];
+ }
+ this.textValue = block.blockData.content || [DefaultContent];
+
+ contentsGroup.appendChild(this.textBox);
+ node.appendChild(this.rect);
+ node.appendChild(this.textBox);
+ group.appendChild(node);
+
+
+ this.rect.setAttributeNS(null, 'class', "node_body");
+ this.rect.setAttributeNS(null, 'x', "0");
+ this.rect.setAttributeNS(null, 'y', "0");
+
+ this.updateStyle();
+ this._updateTextBox();
+ this._updateSize();
+
+ if (initOps.workspace) {
+ this.workspace = initOps.workspace;
+ this.handle = new UiElementHandle(this, node, initOps.workspace, ['adjust_settings']);
+ }
+ }
+
+ init() {
+ if (this.handle) {
+ this.handle.init();
+ }
+ }
+
+
+ getArea(): Area2D {
+ return this.getBodyElement().getBBox();
+ }
+
+ isTextEditable(): this is TextEditable {
+ return false;
+ }
+
+ isTextReadable(): this is TextReadable {
+ return true;
+ }
+
+ get isStaticText(): boolean {
+ return true;
+ }
+
+ get editableTextName(): string {
+ return 'contents';
+ }
+
+ public get text(): string {
+ return this.contentBox.innerText;
+ }
+
+ dispose() {}
+
+ onInputUpdated(block: FlowBlock, inputIndex: number) {}
+
+ onConnectionValueUpdate(inputIndex: number, value: string) {}
+
+ onConnectionLost(portIndex: number) { }
+
+ // Focus management
+ onClick() {
+ // TODO: Double click for edition?
+ }
+
+ onGetFocus() {
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.show();
+ }
+ }
+
+ onLoseFocus() {
+ if (this.contentBox) {
+ this.contentBox.blur();
+ }
+ if (!this.handle) {
+ throw new Error("Cannot show manipulators as workspace has not been received.");
+ }
+ else {
+ this.handle.hide();
+ }
+ }
+
+ // Handleable element
+ doesTakeAllHorizontal() {
+ return false;
+ }
+
+ isAutoresizable(): this is Autoresizable {
+ return false;
+ }
+
+ getMinSize() {
+ return { width: this.MinWidth, height: this.MinHeight };
+ }
+
+ getBodyArea(): Area2D {
+ return this.block.getBodyArea();
+ }
+
+ getBodyElement(): SVGRectElement {
+ return this.rect;
+ }
+
+ getBlock(): FlowBlock {
+ return this.block;
+ }
+
+ updateOptions() {
+ if ((this.block.blockData.textContent) && !(this.block.blockData.content)) {
+ this.block.blockData.content = [{ type: 'text', value: this.block.blockData.textContent }];
+ }
+ this.textValue = this.block.blockData.content || [DefaultContent];
+
+ this._updateTextBox();
+ this._updateSize();
+
+ this._applyConfiguration(this.block.blockData.settings || {});
+ }
+
+ // Configurable element
+ startAdjustingSettings(): void {
+ this.block.workspace.startBlockConfiguration(this);
+ }
+
+ _applyConfiguration(settings: BlockConfigurationOptions): void {
+ const settingsStorage = Object.assign({}, this.block.blockData.settings || {});
+
+ if (settings.text) {
+
+ if (!settingsStorage.text) {
+ settingsStorage.text = {};
+ }
+
+ if (settings.text.color) {
+ settingsStorage.text.color = {value: settings.text.color.value};
+ }
+ if (settings.text.fontSize) {
+ settingsStorage.text.fontSize = {value: settings.text.fontSize.value};
+ }
+ }
+
+
+ settingsStorage.bg = settings.bg;
+
+ this.block.blockData.settings = settingsStorage;
+ this.updateStyle();
+ this._updateSize({ anchor: 'bottom-center' }); // Style changes might change the block's size
+
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+
+ applyConfiguration(settings: BlockConfigurationOptions): void {
+ this._applyConfiguration(settings);
+
+ this.block.notifyOptionsChange();
+ }
+
+ getCurrentConfiguration(): BlockConfigurationOptions {
+ const config = Object.assign({}, this.block.blockData.settings || {});
+
+ // Seed default configuration if not already there
+ if (!config.bg) {
+ config.bg = { type: 'color', value: DEFAULT_BACKGROUND_COLOR };
+ }
+ if (!config.text) {
+ config.text = {};
+ }
+ if (!config.text.color) {
+ config.text.color = {value: DEFAULT_TEXT_COLOR};
+ }
+
+ return config;
+ }
+
+ getAllowedConfigurations(): BlockAllowedConfigurations {
+ return {
+ text: {
+ color: true,
+ fontSize: true,
+ },
+ background: {
+ color: true,
+ image: false,
+ }
+ };
+ }
+
+ // When inside a container, avoid overflowing it
+ updateContainer(container: UiFlowBlock | null) {
+ if (container instanceof ContainerFlowBlock) {
+ this._container = container;
+ }
+ else {
+ this._container = null;
+ }
+ this._updateSize();
+ }
+
+ dropOnEndMove() {
+ if (!this.editing) {
+ this._updateTextBox();
+ this._updateSize();
+ }
+ return { x: 0, y: 0 };
+ }
+
+ // Style management
+ updateStyle() {
+ const settings = this.block.blockData.settings;
+ if (!settings) {
+ return;
+ }
+
+ if (settings.text) {
+ if (settings.text.color) {
+ this.textBox.style.color = settings.text.color.value;
+ }
+ if (settings.text.fontSize) {
+ this.textBox.style.fontSize = settings.text.fontSize.value + 'px';
+ }
+ }
+
+ if (settings.bg) {
+ // Get color to apply
+ let color = DEFAULT_BACKGROUND_COLOR;
+ if (settings.bg.type === 'color') {
+ color = settings.bg.value;
+ }
+ else if (settings.bg.type === 'transparent') {
+ color = 'transparent';
+ }
+
+ // Apply it to the element's background
+ this.rect.style.fill = color;
+ }
+
+ }
+
+ // Aux
+ onContentEditStart() {
+ this.textBox.setAttributeNS(null, 'y', "");
+ this.textBox.setAttributeNS(null, 'x', "");
+
+ const width = this.rect.getAttributeNS(null, 'width');
+ const height = this.rect.getAttributeNS(null, 'height');
+ this.textBox.setAttributeNS(null, 'width', width);
+ this.textBox.setAttributeNS(null, 'height', height);
+
+ this.contentBox.style.height = height + 'px';
+ this.contentBox.style.maxWidth = '';
+ this.contentBox.style.width = width + 'px';
+ this.contentBox.style.height = height + 'px';
+ this.contentBox.classList.add('editing');
+
+ this.editing = true;
+
+ startOnElementEditor(this.contentBox, this.textBox, this.block.workspace.getDialog(),
+ (tt: FormattedTextTree) => {
+ this.block.blockData.content = this.textValue = tt;
+
+ this.editing = false;
+ this._updateTextBox();
+ this._updateSize();
+ if (this.workspace) {
+ this.workspace.invalidateBlock(this.block.id);
+ }
+ },
+ (width: number, height: number) => {
+ width = Math.min(MAX_WIDTH, Math.max(width, this.MinWidth));
+ height = Math.max(height, this.MinHeight);
+
+ const zoom = this.workspace ? this.workspace.getInvZoomLevel() : 1;
+
+ this.rect.setAttributeNS(null, 'width', width * zoom + '');
+ this.rect.setAttributeNS(null, 'height', height * zoom + '');
+ this.textBox.setAttributeNS(null, 'width', width * zoom + '');
+ this.textBox.setAttributeNS(null, 'height', height * zoom + '');
+
+ this.contentBox.style.width = width * zoom + 'px';
+ this.contentBox.style.height = height * zoom + 'px';
+ });
+ }
+
+ _updateTextBox() {
+ this.textBox.innerHTML = '';
+
+ this.contentBox = document.createElement('div');
+ this.contentBox.style.width = 'max-content';
+
+ if (this.initOps.workspace) {
+ // Don't make editable on exhibitor
+ this.contentBox.contentEditable = 'true';
+ }
+ this.contentBox.onfocus = this.onContentEditStart.bind(this);
+ this.contentBox.onmousedown = (ev: MouseEvent) => {
+ ev.stopImmediatePropagation();
+ }
+
+ const container = document.createElement('div');
+ const content = formattedTextTreeToDom(this.textValue);
+
+ container.appendChild(content);
+ this.contentBox.appendChild(container);
+
+ // Give all available width
+ this.contentBox.style.maxWidth = MAX_WIDTH + 'px';
+ this.contentBox.style.width = 'max-content';
+
+ this.textBox.setAttributeNS(null, 'width', MAX_WIDTH + '');
+
+ // Then add it to the ForeignObject
+ this.textBox.appendChild(this.contentBox);
+
+ this._updateFullTextArea();
+ }
+
+ _updateFullTextArea() {
+ const textArea = this.contentBox.getBoundingClientRect();
+ const zoom = this.workspace ? this.workspace.getInvZoomLevel() : 1;
+
+ this.fullTextArea = {
+ x: textArea.x,
+ y: textArea.y,
+ width: textArea.width * zoom,
+ height: textArea.height * zoom,
+ };
+ }
+
+ _updateSize(opts?: { anchor?: 'bottom-center' | 'top-left' }) {
+ // Obtain size taken by all the text
+ const zoom = this.workspace ? this.workspace.getInvZoomLevel() : 1;
+ const anchor = opts && opts.anchor ? opts.anchor : 'top-left';
+
+ const oldHeight = this.rect.height.baseVal.value;
+ const oldWidth = this.rect.width.baseVal.value;
+ const box_height = Math.max(this.fullTextArea.height + 25 * zoom, this.MinHeight);
+ const box_width = Math.min(MAX_WIDTH, Math.max(this.fullTextArea.width + 50 * zoom, this.MinWidth));
+
+ if (anchor === 'bottom-center') {
+ // Move the box around to respect the anchor point
+ this.block.moveBy({
+ x: -((box_width - oldWidth) / 2),
+ y: -(box_height - oldHeight),
+ })
+ }
+
+ this.textBox.setAttributeNS(null, 'x', (box_width - this.fullTextArea.width)/2 + "");
+ this.textBox.setAttributeNS(null, 'y', (box_height - this.fullTextArea.height)/2 + "");
+ this.textBox.setAttributeNS(null, 'width', box_width + "");
+ this.textBox.setAttributeNS(null, 'height', this.fullTextArea.height + "");
+
+ this.rect.setAttributeNS(null, 'height', box_height + "");
+ this.rect.setAttributeNS(null, 'width', box_width + "");
+
+ if (this.handle) {
+ this.handle.update();
+ }
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/ui_element_handle.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/ui_element_handle.ts
new file mode 100644
index 00000000..7ab9290f
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/ui_element_handle.ts
@@ -0,0 +1,259 @@
+import { Position2D, FlowBlock, Resizeable } from "../../flow_block";
+import { FlowWorkspace } from "../../flow_workspace";
+import { ConfigurableBlock } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+const ManipulatorButtonSize = '200';
+const HEIGHT_MANIPULATOR_VERTICAL_PADDING = 10;
+const WIDTH_MANIPULATOR_HORIZONTAL_PADDING = 10;
+const RESIZE_MANIPULATOR_SIZE = 35;
+const SETTINGS_MANIPULATOR_SIZE = 35;
+const MANIPULATOR_ICON_PADDING = 5;
+
+export type HandleOption
+ = 'resize_width_height'
+ | 'resize_height'
+ | 'resize_width'
+ | 'adjust_settings'
+;
+
+export interface HandleableElement {
+ getBodyElement: () => SVGElement;
+ getBlock: () => FlowBlock;
+}
+
+export interface ConfigurableSettingsElement extends ConfigurableBlock {
+ startAdjustingSettings(): void;
+}
+
+function gen_width_height_resize_icon(size: number): SVGElement {
+ const element = document.createElementNS(SvgNS, 'path');
+
+ const far = size - MANIPULATOR_ICON_PADDING;
+ const near = MANIPULATOR_ICON_PADDING;
+
+ element.setAttributeNS(null, 'd', `M${far},${far} V${near} C ${far},${near} ${far * .75},${far * .75} ${near},${far} Z`);
+
+ return element;
+}
+
+function gen_height_resize_icon(size: number): SVGElement {
+ const element = document.createElementNS(SvgNS, 'path');
+
+ const center = size/2;
+ const far = size - MANIPULATOR_ICON_PADDING;
+ const near = MANIPULATOR_ICON_PADDING;
+
+ const cols = (size - MANIPULATOR_ICON_PADDING * 2) / 3;
+
+ const col1 = MANIPULATOR_ICON_PADDING + cols;
+ const col2 = col1 + cols;
+
+ element.setAttributeNS(null, 'd', `M${col1},${near} V${center} H${near} L${center},${far} L${far},${center} H${col2} V${near} H${far} H${near} `);
+
+ return element;
+}
+
+function gen_width_resize_icon(size: number): SVGElement {
+ const element = document.createElementNS(SvgNS, 'path');
+
+ const center = size/2;
+ const far = size - MANIPULATOR_ICON_PADDING;
+ const near = MANIPULATOR_ICON_PADDING;
+
+ const cols = (size - MANIPULATOR_ICON_PADDING * 2) / 3;
+
+ const col1 = MANIPULATOR_ICON_PADDING + cols;
+ const col2 = col1 + cols;
+
+ element.setAttributeNS(null, 'd', `M${near},${col1} H${center} V${near} L${far},${center} L${center},${far} V${col2} H${near} V${far} V${near} `);
+
+ return element;
+}
+
+function gen_settings_manipulator_icon(size: number): SVGElement {
+ const element = document.createElementNS(SvgNS, 'image');
+
+ element.setAttributeNS(null, 'href', '/assets/icons/settings.svg');
+ element.setAttributeNS(null, 'width', size + '');
+ element.setAttributeNS(null, 'height', size + '');
+
+ return element;
+}
+
+export class UiElementHandle {
+ handleGroup: SVGGElement;
+ resizePrevPos: Position2D;
+ settingsManipulator: SVGGElement;
+
+ widthHeightResizeManipulator: SVGElement;
+ heightResizeManipulator: SVGGElement;
+ widthResizeManipulator: SVGGElement;
+
+ constructor(private element: HandleableElement,
+ private root: SVGGElement,
+ private workspace: FlowWorkspace,
+ private handleOptions: HandleOption[]) {}
+
+ init() {
+ this.handleGroup = document.createElementNS(SvgNS, 'g');
+
+ this.handleGroup.setAttribute('class', 'manipulators hidden');
+
+ if (this.handleOptions.indexOf('resize_width_height') >= 0) {
+ const m = this.widthHeightResizeManipulator = document.createElementNS(SvgNS, 'g');
+
+ const iconBackground = document.createElementNS(SvgNS, 'rect');
+ iconBackground.setAttribute('class', 'handle-icon-background');
+ iconBackground.setAttributeNS(null, 'rx', '4');
+ iconBackground.setAttributeNS(null, 'width', RESIZE_MANIPULATOR_SIZE + '');
+ iconBackground.setAttributeNS(null, 'height', RESIZE_MANIPULATOR_SIZE + '');
+ m.appendChild(iconBackground);
+
+ const resizeIcon = gen_width_height_resize_icon(RESIZE_MANIPULATOR_SIZE);
+ m.appendChild(resizeIcon);
+ m.onmousedown = this._startResizeWidthHeight.bind(this);
+ m.ontouchstart = this._startResizeWidthHeight.bind(this);
+
+ m.setAttribute('class', 'manipulator resize-manipulator width-height-resizer');
+ this.handleGroup.appendChild(m);
+ }
+ else if (this.handleOptions.indexOf('resize_height') >= 0) {
+ this._createHeightResizeManipulator();
+ }
+ else if (this.handleOptions.indexOf('resize_width') >= 0) {
+ this. _createWidthResizeManipulator();
+ }
+
+ if (this.handleOptions.indexOf('adjust_settings') >= 0) {
+ const m = this.settingsManipulator = document.createElementNS(SvgNS, 'g');
+
+ const iconSettings = document.createElementNS(SvgNS, 'rect');
+ iconSettings.setAttribute('class', 'handle-icon-background');
+ iconSettings.setAttributeNS(null, 'rx', '4');
+ iconSettings.setAttributeNS(null, 'width', SETTINGS_MANIPULATOR_SIZE + '');
+ iconSettings.setAttributeNS(null, 'height', SETTINGS_MANIPULATOR_SIZE + '');
+ m.appendChild(iconSettings);
+
+ const resizeIcon = gen_settings_manipulator_icon(SETTINGS_MANIPULATOR_SIZE);
+ m.appendChild(resizeIcon);
+ m.onmousedown = this._startUpdateSettings.bind(this);
+ m.ontouchstart = this._startUpdateSettings.bind(this);
+
+ m.setAttribute('class', 'manipulator settings-manipulator');
+ this.handleGroup.appendChild(m);
+ }
+
+ this.root.appendChild(this.handleGroup); // Avoid the manipulators affecting the element
+ }
+
+ show() {
+ this.handleGroup.classList.remove('hidden');
+ this.update()
+ }
+
+ hide() {
+ this.handleGroup.classList.add('hidden');
+ }
+
+ update() {
+ this._reposition();
+ }
+
+ private _createWidthResizeManipulator() {
+ const m = this.widthResizeManipulator = document.createElementNS(SvgNS, 'g');
+
+ const iconBackground = document.createElementNS(SvgNS, 'rect');
+ iconBackground.setAttribute('class', 'handle-icon-background');
+ iconBackground.setAttributeNS(null, 'rx', '4');
+ iconBackground.setAttributeNS(null, 'width', RESIZE_MANIPULATOR_SIZE + '');
+ iconBackground.setAttributeNS(null, 'height', RESIZE_MANIPULATOR_SIZE + '');
+ m.appendChild(iconBackground);
+
+ const resizeIcon = gen_width_resize_icon(RESIZE_MANIPULATOR_SIZE);
+ m.appendChild(resizeIcon);
+ m.onmousedown = this._startResizeWidth.bind(this);
+ m.ontouchstart = this._startResizeWidth.bind(this);
+
+ m.setAttribute('class', 'manipulator resize-manipulator width-resizer');
+ this.handleGroup.appendChild(m);
+ }
+
+ private _createHeightResizeManipulator() {
+ const m = this.heightResizeManipulator = document.createElementNS(SvgNS, 'g');
+
+ const iconBackground = document.createElementNS(SvgNS, 'rect');
+ iconBackground.setAttribute('class', 'handle-icon-background');
+ iconBackground.setAttributeNS(null, 'rx', '4');
+ iconBackground.setAttributeNS(null, 'width', RESIZE_MANIPULATOR_SIZE + '');
+ iconBackground.setAttributeNS(null, 'height', RESIZE_MANIPULATOR_SIZE + '');
+ m.appendChild(iconBackground);
+
+ const resizeIcon = gen_height_resize_icon(RESIZE_MANIPULATOR_SIZE);
+ m.appendChild(resizeIcon);
+ m.onmousedown = this._startResizeHeight.bind(this);
+
+ m.setAttribute('class', 'manipulator resize-manipulator height-resizer');
+ this.handleGroup.appendChild(m);
+ }
+
+ // Event handling
+ private _startResizeWidthHeight(ev: MouseEvent | TouchEvent) {
+ ev.preventDefault(); // Avoid triggering default events
+ ev.stopImmediatePropagation(); // Avoid triggering other custom events up-tree
+
+ this.workspace.startResizing(this.element as any as Resizeable, ev);
+
+ return true;
+ }
+
+ private _startResizeHeight(ev: MouseEvent | TouchEvent) {
+ ev.preventDefault(); // Avoid triggering default events
+ ev.stopImmediatePropagation(); // Avoid triggering other custom events up-tree
+
+ // TODO: Add resize type
+ this.workspace.startResizing(this.element as any as Resizeable, ev);
+
+ return true;
+ }
+
+ private _startResizeWidth(ev: MouseEvent | TouchEvent) {
+ ev.preventDefault(); // Avoid triggering default events
+ ev.stopImmediatePropagation(); // Avoid triggering other custom events up-tree
+
+ // TODO: Add resize type
+ this.workspace.startResizing(this.element as any as Resizeable, ev);
+
+ return true;
+ }
+
+ private _startUpdateSettings(ev: MouseEvent | TouchEvent){
+ ev.preventDefault(); // Avoid triggering default events
+ ev.stopImmediatePropagation(); // Avoid triggering other custom events up-tree
+
+ (this.element as any as ConfigurableSettingsElement).startAdjustingSettings();
+
+ return true;
+ }
+
+ private _reposition() {
+ const box = this.element.getBlock().getBodyArea();
+
+ if (this.widthHeightResizeManipulator) {
+ this.widthHeightResizeManipulator.setAttributeNS(null, 'transform', `translate(${box.width}, ${box.height})`);
+ }
+
+ if (this.heightResizeManipulator) {
+ this.heightResizeManipulator.setAttributeNS(null, 'transform', `translate(${box.width / 2}, ${box.height + HEIGHT_MANIPULATOR_VERTICAL_PADDING})`);
+ }
+
+ if (this.widthResizeManipulator) {
+ this.widthResizeManipulator.setAttributeNS(null, 'transform', `translate(${box.width + WIDTH_MANIPULATOR_HORIZONTAL_PADDING}, ${box.height / 2})`);
+ }
+
+if (this.settingsManipulator) {
+ this.settingsManipulator.setAttributeNS(null, 'transform', `translate(${box.width}, ${0})`);
+ }
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/ui_tree_repr.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/ui_tree_repr.ts
new file mode 100644
index 00000000..6ab9cc4b
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/ui_tree_repr.ts
@@ -0,0 +1,89 @@
+import { Area2D } from "../../flow_block";
+import { BlockConfigurationOptions } from "../../dialogs/configure-block-dialog/configure-block-dialog.component";
+import { UiFlowBlock } from "../ui_flow_block";
+
+export type BackgroundPropertyConfiguration = { type: 'transparent' } | { type: 'color', value: string };
+
+export type UiElementWidgetType
+ = 'simple_button'
+ | 'fixed_text'
+ | 'text_box'
+ | 'dynamic_text'
+ | 'fixed_image'
+ | 'responsive_page_holder'
+ | 'horizontal_ui_section'
+ | 'horizontal_separator'
+ | 'simple_card'
+ | 'link_area'
+;
+
+export type AtomicUiElementWidget
+ = 'simple_button'
+ | 'fixed_text'
+ | 'dynamic_text'
+ | 'fixed_image'
+ | 'horizontal_separator'
+;
+
+export type UiElementWidgetContainer
+ = 'simple_card'
+ | 'link_area'
+;
+
+
+export interface UiElementRepr {
+ settings?: BlockConfigurationOptions;
+ dimensions?: { width: number, height: number },
+ id: string,
+ widget_type: AtomicUiElementWidget,
+ text?: string,
+ content?: any,
+};
+
+export interface ContainerElementRepr {
+ id: string,
+ container_type: UiElementWidgetContainer,
+ content: CutTree,
+ settings?: BlockConfigurationOptions,
+};
+
+export interface CutElement {
+ i: number,
+ a: Area2D,
+ b: UiFlowBlock,
+};
+export type CutType = 'vbox' | 'hbox';
+
+export const DEFAULT_CUT_TYPE = 'hbox';
+
+export interface CutNode {
+ settings?: BlockConfigurationOptions,
+ block_id?: string,
+ cut_type: CutType,
+ groups: CutTree[],
+};
+export type CutTree = UiElementRepr | ContainerElementRepr | CutNode;
+
+export function isAtomicUiElementType(uiElement: UiElementWidgetType): uiElement is AtomicUiElementWidget {
+ switch(uiElement) {
+ case 'simple_button':
+ case 'fixed_text':
+ case 'dynamic_text':
+ case 'fixed_image':
+ case 'horizontal_separator':
+ case 'text_box':
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+export function widgetAsAtomicUiElement(uiElement: UiElementWidgetType): AtomicUiElementWidget {
+ if (isAtomicUiElementType(uiElement)) {
+ return uiElement;
+ }
+ else {
+ throw new Error(`Converted value is not an AtomicUiElement: ${uiElement}`);
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/renderers/utils.ts b/frontend/src/app/flow-editor/ui-blocks/renderers/utils.ts
new file mode 100644
index 00000000..e397c2f3
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/renderers/utils.ts
@@ -0,0 +1,546 @@
+import { MatDialog } from "@angular/material/dialog";
+import { ConfigureFontColorDialogComponent } from "../../dialogs/configure-font-color-dialog/configure-font-color-dialog.component";
+import { ConfigureLinkDialogComponent, UnderlineSettings } from "../../dialogs/configure-link-dialog/configure-link-dialog.component";
+import { Area2D, ManipulableArea2D } from "../../flow_block";
+import { extractContentsToRight, isTagOnAncestors, isTagOnTree, surroundRangeWithElement, getUnderlineSettings, applyUnderlineSettings, colorToHex, flattenAllTagsUnder } from "./dom_utils";
+import { uuidv4 } from "../../utils";
+
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+const DEFAULT_FONT_COLOR = '#000000';
+
+export function getRefBox(canvas: SVGElement): DOMRect {
+ const refText = document.createElementNS(SvgNS, 'text');
+ refText.setAttribute('class', 'node_name');
+ refText.setAttributeNS(null,'textlength', '100%');
+
+ refText.setAttributeNS(null, 'x', "0");
+ refText.setAttributeNS(null, 'y', "0");
+ refText.textContent = "test";
+ canvas.appendChild(refText);
+
+ const refBox = refText.getBoundingClientRect();
+
+ canvas.removeChild(refText);
+
+ return refBox;
+}
+
+export function combinedManipulableArea(areas: Area2D[]): ManipulableArea2D {
+ if (areas.length === 0) {
+ return {
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ };
+ }
+
+ const initialArea = areas[0];
+ let rect = {
+ left: initialArea.x,
+ top: initialArea.y,
+ right: initialArea.x + initialArea.width,
+ bottom: initialArea.y + initialArea.height,
+ };
+
+ for (let i = 1; i < areas.length; i++) {
+ const bArea = areas[i];
+
+ let bRect = {
+ left: bArea.x,
+ top: bArea.y,
+ right: bArea.x + bArea.width,
+ bottom: bArea.y + bArea.height,
+ };
+
+ rect.left = Math.min(rect.left, bRect.left);
+ rect.top = Math.min(rect.top, bRect.top);
+ rect.right = Math.max(rect.right, bRect.right);
+ rect.bottom = Math.max(rect.bottom, bRect.bottom);
+ }
+
+ return rect;
+}
+
+export function manipulableAreaToArea2D(area: ManipulableArea2D) {
+ return {
+ x: area.left,
+ y: area.top,
+ width: area.right - area.left,
+ height: area.bottom - area.top,
+ };
+}
+
+export function combinedArea(areas: Area2D[]): Area2D {
+ const combined = combinedManipulableArea(areas);
+
+ return manipulableAreaToArea2D(combined);
+}
+
+type FormatType = 'bold' | 'italic' | 'underline';
+type TextChunk = { type: 'text', value: string };
+type TextColorChunk = { type: 'text-color', color: string, contents: FormattedTextTree };
+type LinkChunk = { type: 'link', target: string, open_in_tab: boolean, underline: UnderlineSettings, contents: FormattedTextTree };
+type FormatChunk = { type: 'format', format: FormatType, contents: FormattedTextTree }
+
+export type FormattedTextTree = (TextChunk | FormatChunk | LinkChunk | TextColorChunk)[];
+
+function getFormatTypeOfElement(el: HTMLElement): FormatType | null {
+ switch (el.tagName.toLowerCase()) {
+ case 'b':
+ case 'strong':
+ return 'bold';
+
+ case 'i':
+ case 'em':
+ return 'italic';
+
+ case 'u':
+ return 'underline';
+
+ default:
+ return null;
+ }
+}
+
+function trimFormattedTextTree(tree: FormattedTextTree): FormattedTextTree {
+ // Note that this is a destructive operation, the inputted text tree will be the same as the outputted one
+ while (tree.length > 0) {
+ const last = tree[tree.length - 1];
+
+ // If the last element is a whitespace, remove it
+ if (last.type === 'text' && last.value.trim() === '') {
+ tree.pop();
+ }
+ else {
+ break;
+ }
+ }
+
+ return tree;
+}
+
+function formatTypeToElement(ft: FormatType): string {
+ switch (ft) {
+ case 'bold':
+ return 'b';
+
+ case 'italic':
+ return 'i';
+
+ case 'underline':
+ return 'u';
+
+ default:
+ return null;
+ }
+}
+
+export function domToFormattedTextTree(node: Node) : FormattedTextTree {
+ if (node instanceof Text) {
+ return [{ type: 'text', value: node.textContent }];
+ }
+ else if (node instanceof HTMLElement) {
+ const formatType = getFormatTypeOfElement(node);
+ let subTrees: FormattedTextTree = [];
+
+ for (const n of Array.from(node.childNodes)){
+ subTrees = subTrees.concat(domToFormattedTextTree(n));
+ }
+
+ if (node instanceof HTMLDivElement) {
+ subTrees.push({ type: 'text', value: '\n' });
+ }
+
+ if (node instanceof HTMLAnchorElement) {
+ const openInTab = node.target && (node.target.indexOf('_blank') >= 0);
+ return [{
+ type: 'link',
+ target: node.href,
+ open_in_tab: !!openInTab,
+ underline: getUnderlineSettings(node),
+ contents: subTrees,
+ }];
+ }
+
+ if (node instanceof HTMLFontElement) {
+ return [{ type: 'text-color', color: node.style.color, contents: subTrees }];
+ }
+
+ if (!formatType) {
+ return subTrees;
+ }
+ else {
+ return [{ type: 'format', format: formatType, contents: subTrees }];
+ }
+ }
+ else {
+ throw Error("Unexpected node type: " + node);
+ }
+}
+
+function unwrapSpan(node: Node): Node[] {
+ if (node instanceof HTMLSpanElement) {
+ let elements: Node[] = [];
+ for (const e of Array.from(node.childNodes)) {
+ const unwrapped = unwrapSpan(e);
+ if (unwrapped.length === 0) {
+ // Nothing to do
+ }
+ else if (unwrapped.length === 1){
+ // No need to create a new list
+ elements.push(unwrapped[0]);
+ }
+ else {
+ elements = elements.concat(unwrapped);
+ }
+ }
+
+ return elements;
+ }
+ else {
+ return [node];
+ }
+}
+
+export function formattedTextTreeToDom(tt: FormattedTextTree, nested?: boolean): Node {
+ const nodes = [];
+
+ let group = document.createElement(nested ? 'span' : 'div');
+ nodes.push(group);
+
+ for (const el of tt) {
+ if (el.type === 'text') {
+ if (el.value === '\n') {
+ if (group && (group.tagName.toLowerCase() === 'div') && group.innerText == '') {
+ group.innerHTML = ' ';
+ }
+
+ group = document.createElement('div');
+ nodes.push(group);
+ }
+ else {
+ const node = document.createTextNode(el.value);
+
+ group.appendChild(node);
+ }
+ }
+ else if (el.type === 'format') {
+ const node = document.createElement(formatTypeToElement(el.format));
+
+ const contents = formattedTextTreeToDom(el.contents, true);
+
+ node.append(...unwrapSpan(contents));
+
+ group.appendChild(node);
+ }
+ else if (el.type === 'link') {
+ const node = document.createElement('a');
+
+ node.href = el.target;
+ if (el.open_in_tab) {
+ node.target = '_blank';
+ node.rel = 'noopener noreferrer';
+ }
+ applyUnderlineSettings(node, el.underline);
+ const contents = formattedTextTreeToDom(el.contents, true);
+ node.append(...unwrapSpan(contents));
+
+ group.appendChild(node);
+ }
+ else if (el.type === 'text-color') {
+ const node = document.createElement('font');
+
+ node.style.color = el.color;
+ const contents = formattedTextTreeToDom(el.contents, true);
+ node.append(...unwrapSpan(contents));
+
+ group.appendChild(node);
+ }
+ }
+
+ if (group && (group.tagName.toLowerCase() === 'div') && group.innerText == '') {
+ group.innerHTML = ' ';
+ }
+
+ if (nodes.length === 1) {
+ return nodes[0];
+ }
+
+ const wrapper = document.createElement('div');
+ for (const node of nodes){
+ wrapper.appendChild(node);
+ }
+
+ return wrapper;
+}
+
+function editColorInSelection(dialog: MatDialog): Promise {
+ return new Promise((resolve, reject) => {
+ const sel = window.getSelection();
+ const range = sel.getRangeAt(0);
+ const contents = range.cloneContents();
+ let text = contents.textContent;
+ let fontTag: HTMLFontElement | null = null;
+
+ if (contents.childNodes.length === 0) {
+ const ancestorInfo = isTagOnAncestors(range.commonAncestorContainer, 'font');
+ if (ancestorInfo) {
+ fontTag = ancestorInfo.ancestor as HTMLFontElement;
+ }
+ else {
+ // TODO: Show error notification
+ reject("Cannot edit color in empty selection");
+ }
+ }
+
+ const newWrapper = !fontTag;
+ if (fontTag) {
+ text = fontTag.textContent;
+ }
+ else {
+ fontTag = document.createElement('font');
+ fontTag.style.color = DEFAULT_FONT_COLOR;
+ surroundRangeWithElement(range, fontTag);
+ }
+
+ const dialogRef = dialog.open(ConfigureFontColorDialogComponent, {
+ data: { text: text, color: colorToHex(fontTag.style.color ? fontTag.style.color : DEFAULT_FONT_COLOR) }
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!(result && result.success)) {
+ if (newWrapper) {
+ // Unwrap elements
+ extractContentsToRight(fontTag);
+ }
+ }
+ else if (result.operation === 'remove-color') {
+ extractContentsToRight(fontTag);
+ }
+ else {
+ // Update the tag with the appropriate link
+ fontTag.style.color = result.value.color;
+ }
+
+ // Remove all other tags under the updated one
+ flattenAllTagsUnder(fontTag, 'font');
+
+ resolve();
+ });
+ });
+}
+
+function editLinkInSelection(dialog: MatDialog): Promise {
+ return new Promise((resolve, reject) => {
+ const sel = window.getSelection();
+ const range = sel.getRangeAt(0);
+ const contents = range.cloneContents();
+ let text = contents.textContent;
+ let linkValue = '';
+ let linkTag: HTMLAnchorElement | null = null;
+
+ const ancestorInfo = isTagOnAncestors(range.commonAncestorContainer, 'a');
+ if (ancestorInfo) {
+ linkTag = ancestorInfo.ancestor as HTMLAnchorElement;
+ }
+ else {
+ linkTag = isTagOnTree(contents, 'a') as HTMLAnchorElement;
+ }
+
+ const newWrapper = !linkTag;
+ if (linkTag) {
+ linkValue = linkTag.href;
+ text = linkTag.textContent;
+ }
+ else {
+ linkTag = document.createElement('a');
+ surroundRangeWithElement(range, linkTag);
+ }
+
+ const openInTab = linkTag.target && linkTag.target.indexOf('_blank') >= 0;
+
+ const underline: UnderlineSettings = getUnderlineSettings(linkTag);
+
+ const dialogRef = dialog.open(ConfigureLinkDialogComponent, {
+ data: { text: text, link: linkValue, openInTab: openInTab, underline: underline }
+ });
+
+ dialogRef.afterClosed().subscribe(async (result) => {
+ if (!(result && result.success)) {
+ if (newWrapper) {
+ // Unwrap elements
+ extractContentsToRight(linkTag);
+ }
+ }
+ else if (result.operation === 'remove-link') {
+ extractContentsToRight(linkTag);
+ }
+ else {
+ // Update the tag with the appropriate link
+ linkTag.href = result.value.link;
+ linkTag.innerText = result.value.text;
+
+ if (result.value.openInTab) {
+ linkTag.target = '_blank';
+ linkTag.rel = 'noopener noreferrer';
+ }
+ else {
+ linkTag.target = '';
+ }
+
+ applyUnderlineSettings(linkTag, result.value.underline);
+ }
+
+ resolve();
+ });
+ });
+}
+
+
+const ButtonBarId = 'flow-editor-in-element-button-bar-' + uuidv4();
+
+const ON_ELEMENT_EDITOR_MARGIN = 50;
+
+export function startOnElementEditor(element: HTMLDivElement, parent: SVGForeignObjectElement, dialog: MatDialog, onDone: (text: FormattedTextTree) => void, resize: (width: number, height: number) => void) {
+ const elementPos = element.getBoundingClientRect();
+
+ {
+ const oldButtonBar = document.getElementById(ButtonBarId);
+ if (oldButtonBar) {
+ console.warn("Old button bar found, this should now happen. Removing...")
+ oldButtonBar.parentElement.removeChild(oldButtonBar);
+ }
+ }
+
+ const buttonBar = document.createElement('div');
+ buttonBar.setAttribute('id', ButtonBarId);
+ {
+ buttonBar.classList.add('floating-button-bar');
+
+ const boldButton = document.createElement('button');
+ boldButton.classList.add('bold-button');
+ buttonBar.appendChild(boldButton);
+ boldButton.innerText = 'B';
+ boldButton.onmousedown = (ev) => {
+ document.execCommand('bold', false, undefined);
+ ev.preventDefault(); // Prevent losing focus on element
+ }
+
+ const italicButton = document.createElement('button');
+ italicButton.classList.add('italic-button');
+ buttonBar.appendChild(italicButton);
+ italicButton.innerText = 'I';
+ italicButton.onmousedown = (ev) => {
+ document.execCommand('italic', false, undefined);
+ ev.preventDefault(); // Prevent losing focus on element
+ }
+
+ const underlineButton = document.createElement('button');
+ underlineButton.classList.add('underline-button');
+ buttonBar.appendChild(underlineButton);
+ underlineButton.innerText = 'U';
+ underlineButton.onmousedown = (ev) => {
+ document.execCommand('underline', false, undefined);
+ ev.preventDefault(); // Prevent losing focus on element
+ }
+
+ const colorButton = document.createElement('button');
+ colorButton.classList.add('color-button');
+ buttonBar.appendChild(colorButton);
+ colorButton.innerHTML = ' ';
+ colorButton.onmousedown = (ev) => {
+ ev.preventDefault(); // Prevent losing focus on element
+ withMovingFocus(() => editColorInSelection(dialog));
+ }
+
+ const linkButton = document.createElement('button');
+ linkButton.classList.add('link-button');
+ buttonBar.appendChild(linkButton);
+ linkButton.innerHTML = ' ';
+ linkButton.onmousedown = (ev) => {
+ ev.preventDefault(); // Prevent losing focus on element
+ withMovingFocus(() => editLinkInSelection(dialog));
+ }
+
+ document.body.appendChild(buttonBar);
+
+ const buttonDim = buttonBar.getBoundingClientRect();
+ buttonBar.style.top = elementPos.y - buttonDim.height + 'px';
+ buttonBar.style.left = elementPos.x + 'px';
+ }
+
+ const updateSize = () => {
+ // Update size
+ const area = (element.firstChild as HTMLElement).getBoundingClientRect();
+ resize(
+ area.width + ON_ELEMENT_EDITOR_MARGIN,
+ area.height + ON_ELEMENT_EDITOR_MARGIN,
+ );
+ }
+
+ element.oninput = updateSize;
+ updateSize();
+
+ element.onkeydown = (ev: KeyboardEvent) => {
+ if (ev.ctrlKey && ev.code === 'KeyB') {
+ ev.preventDefault();
+ document.execCommand('bold', false, undefined);
+ }
+ else if (ev.ctrlKey && ev.code === 'KeyU') {
+ ev.preventDefault();
+ document.execCommand('underline', false, undefined);
+ }
+ else if (ev.ctrlKey && ev.code === 'KeyI') {
+ ev.preventDefault();
+ document.execCommand('italic', false, undefined);
+ }
+ else if (ev.ctrlKey && ev.code === 'KeyK') {
+ ev.preventDefault();
+ withMovingFocus(() => editLinkInSelection(dialog));
+ }
+ else if (ev.ctrlKey && ev.code === 'Enter') {
+ ev.preventDefault();
+ element.blur(); // Release focus
+ }
+ }
+
+ const onBlur = (_ev: FocusEvent) => {
+ // Cleanup
+ element.onkeydown = element.onblur = element.oninput = null;
+ document.body.removeChild(buttonBar);
+
+ onDone(trimFormattedTextTree(domToFormattedTextTree(element)));
+ };
+
+ const withMovingFocus = (f: () => Promise ) => {
+ element.onblur = null;
+ const wasFocus = element.onfocus;
+ element.onfocus = null;
+ f()
+ .catch(err => {
+ // TODO: Show this as a notification
+ console.error(err);
+ })
+ .then(() => {
+ element.focus();
+ element.onfocus = wasFocus;
+ element.onblur = onBlur;
+ });
+ };
+
+ element.onblur = onBlur;
+}
+
+export function listToDict(list: T[], getKey: (elem: T) => string): {[key: string]: T} {
+ const result: {[key: string]: T} = {};
+
+ for (const elem of list) {
+ const key = getKey(elem);
+ result[key] = elem;
+ }
+
+ return result;
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/ui_flow_block.ts b/frontend/src/app/flow-editor/ui-blocks/ui_flow_block.ts
new file mode 100644
index 00000000..04bee32e
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/ui_flow_block.ts
@@ -0,0 +1,753 @@
+import { UiSignalService } from '../../services/ui-signal.service';
+import { BlockManager } from '../block_manager';
+import { Area2D, BlockContextAction, Direction2D, FlowBlock, FlowBlockData, FlowBlockInitOpts, FlowBlockOptions, InputPortDefinition, Position2D, Movement2D, Resizeable } from '../flow_block';
+import { FlowWorkspace } from '../flow_workspace';
+import { Toolbox } from '../toolbox';
+import { CutTree, UiElementWidgetType, UiElementRepr, widgetAsAtomicUiElement } from './renderers/ui_tree_repr';
+import { BlockConfigurationOptions } from '../dialogs/configure-block-dialog/configure-block-dialog.component';
+
+const SvgNS = "http://www.w3.org/2000/svg";
+
+export type UiFlowBlockType = 'ui_flow_block';
+export const BLOCK_TYPE = 'ui_flow_block';
+
+const INPUT_PORT_REAL_SIZE = 10;
+const OUTPUT_PORT_REAL_SIZE = 10;
+
+export interface UiFlowBlockBuilderInitOps {
+ workspace?: FlowWorkspace,
+}
+
+export type UiFlowBlockBuilder = (canvas: SVGElement,
+ group: SVGElement,
+ block: UiFlowBlock,
+ service: UiSignalService,
+ ops: UiFlowBlockBuilderInitOps) => UiFlowBlockHandler;
+
+export interface UiFlowBlockHandler {
+ updateOptions(): void;
+ readonly isNotHorizontallyStackable?: boolean;
+ onConnectionLost: (portIndex: number) => void;
+ onConnectionValueUpdate : (input_index: number, value: string) => void;
+ onClick: () => void,
+ onInputUpdated: (connectedBlock: FlowBlock, inputIndex: number) => void,
+ dispose: () => void,
+ isTextEditable(): this is TextEditable,
+ isTextReadable(): this is TextReadable,
+ getBodyElement(): SVGGraphicsElement,
+
+ isAutoresizable?: () => this is Autoresizable;
+ dropOnEndMove?: () => Movement2D;
+ updateContainer?: (container: UiFlowBlock) => void;
+ onGetFocus?: () => void;
+ onLoseFocus?: () => void;
+}
+
+export interface Autoresizable extends Resizeable {
+ isAutoresizable: () => this is Autoresizable;
+ getMinSize(): { width: number, height: number },
+ doesTakeAllHorizontal: () => boolean,
+ doesTakeAllVertical: () => boolean,
+};
+
+export interface TextReadable {
+ readonly text: string,
+ isStaticText: boolean,
+};
+
+export interface TextEditable extends TextReadable {
+ text: string,
+ getArea(): Area2D,
+ editableTextName: string,
+};
+
+export type OnUiFlowBlockClick = (block: UiFlowBlock, service: UiSignalService) => void;
+export type OnUiFlowBlockInputUpdated = (block: UiFlowBlock, service: UiSignalService, connectedBlock: FlowBlock, inputIndex: number) => void;
+export type OnDispose = () => void;
+
+export interface UiFlowBlockOptions extends FlowBlockOptions {
+ builder: UiFlowBlockBuilder,
+ type: UiFlowBlockType,
+ icon?: string,
+ id: UiElementWidgetType,
+ block_id?: string,
+}
+
+export interface UiFlowBlockExtraData {
+ textContent?: string,
+ content?: any,
+ dimensions?: { width: number, height: number },
+ settings?: BlockConfigurationOptions,
+}
+
+export interface UiFlowBlockData extends FlowBlockData {
+ type: UiFlowBlockType,
+ value: {
+ options: UiFlowBlockOptions,
+ extra: UiFlowBlockExtraData,
+ },
+}
+
+
+export function isUiFlowBlockOptions(opt: FlowBlockOptions): opt is UiFlowBlockOptions {
+ return ((opt as UiFlowBlockOptions).type === BLOCK_TYPE);
+}
+
+export function isUiFlowBlockData(opt: FlowBlockData): opt is UiFlowBlockData {
+ return opt.type === BLOCK_TYPE;
+}
+
+export class UiFlowBlock implements FlowBlock {
+ private canvas: SVGElement;
+ options: UiFlowBlockOptions;
+ readonly id: string;
+ readonly onMoveCallbacks: ((pos: Position2D) => void)[] = [];
+
+ private group: SVGGElement;
+ protected position: {x: number, y: number};
+ private output_groups: SVGGElement[];
+ private input_groups: SVGGElement[];
+ private input_count: number[] = [];
+ protected handler: UiFlowBlockHandler;
+ private input_blocks: [FlowBlock, number][] = [];
+ protected _workspace: FlowWorkspace | null;
+ private _container: UiFlowBlock;
+
+ blockData: UiFlowBlockExtraData = {};
+
+ constructor(options: UiFlowBlockOptions,
+ blockId: string,
+ private uiSignalService: UiSignalService,
+ ) {
+ this.id = blockId;
+ this.options = options;
+ if (!this.options.outputs) {
+ this.options.outputs = [];
+ }
+ if (!this.options.inputs) {
+ this.options.inputs = [];
+ }
+ this.output_groups = [];
+ this.input_groups = [];
+ }
+
+ public dispose() {
+ this.canvas.removeChild(this.group);
+
+ this.handler.dispose();
+ }
+
+ public render(canvas: SVGElement, initOpts: FlowBlockInitOpts): SVGElement {
+ if (this.group) { return this.group } // Avoid double initialization
+ this._workspace = initOpts.workspace;
+
+ this.canvas = canvas;
+ if (initOpts.position) {
+ this.position = { x: initOpts.position.x, y: initOpts.position.y };
+ }
+ else {
+ if (this.options.inputs && this.options.inputs.length > 0) {
+ this.position = {x: 0, y: INPUT_PORT_REAL_SIZE};
+ }
+ else {
+ this.position = {x: 0, y: 0};
+ }
+ }
+
+ const group = document.createElementNS(SvgNS, 'g');
+ this.canvas.appendChild(group);
+
+ this.handler = this.options.builder(canvas, group, this, this.uiSignalService, {
+ workspace: initOpts.workspace,
+ });
+ this._renderOutputs(group);
+ this._renderInputs(group);
+
+ this.group = group;
+ this.group.onclick = this.onclick.bind(this);
+
+ this.moveBy({x: 0, y: 0}); // Apply transformation
+
+ return this.group;
+ }
+
+ public renderAsUiElement(): CutTree {
+ const data: UiElementRepr = { id: this.id, widget_type: widgetAsAtomicUiElement(this.options.id) };
+
+ if (this.handler.isTextReadable()) {
+ if (this.handler.isStaticText) {
+ data.text = this.handler.text;
+ }
+ }
+ if (this.blockData.content) {
+ data.content = this.blockData.content;
+ }
+ if (this.blockData.dimensions) {
+ data.dimensions = this.blockData.dimensions;
+ }
+
+ data.settings = this.blockData.settings;
+
+ return data;
+ }
+
+ private _renderOutputs(group: SVGGElement) {
+ const nodeBox = group.getBoundingClientRect();
+
+ // Add outputs
+ let output_index = -1;
+
+ const output_initial_x_position = 10; // px
+ const outputs_x_margin = 10; // px
+ const output_plating_x_margin = 3; // px
+
+ let output_x_position = output_initial_x_position;
+
+ for (const output of this.options.outputs) {
+ output_index++;
+
+ const out_group = document.createElementNS(SvgNS, 'g');
+ out_group.classList.add('output');
+ group.appendChild(out_group);
+
+ const port_external = document.createElementNS(SvgNS, 'circle');
+ const port_internal = document.createElementNS(SvgNS, 'circle');
+
+ out_group.appendChild(port_external);
+ out_group.appendChild(port_internal);
+
+ const output_port_size = 50;
+ const output_port_internal_size = 5;
+ const output_position_start = output_x_position;
+ let output_position_end = output_x_position + output_port_size;
+
+
+ if (output.name) {
+ // Bind output name and port
+ const port_plating = document.createElementNS(SvgNS, 'rect');
+ out_group.appendChild(port_plating);
+
+ const text = document.createElementNS(SvgNS, 'text');
+ text.textContent = output.name;
+ text.setAttributeNS(null, 'class', 'argument_name output');
+ out_group.appendChild(text);
+
+ output_position_end = Math.max(output_position_end, (output_x_position
+ + text.getBoundingClientRect().width
+ + output_plating_x_margin * 2));
+
+ const output_width = output_position_end - output_position_start;
+
+ text.setAttributeNS(null, 'x', output_position_start + output_width/2 - text.getBoundingClientRect().width/2 + '');
+ text.setAttributeNS(null, 'y', nodeBox.height - (OUTPUT_PORT_REAL_SIZE/2) + '' );
+
+ output_x_position = output_position_end + outputs_x_margin;
+
+ const output_height = Math.max(output_port_size / 2, (OUTPUT_PORT_REAL_SIZE
+ + text.getBoundingClientRect().height));
+
+ // Configure port connector now that we know where the output will be positioned
+ port_plating.setAttributeNS(null, 'class', 'port_plating');
+ port_plating.setAttributeNS(null, 'x', output_position_start + '');
+ port_plating.setAttributeNS(null, 'y', nodeBox.height - output_height/1.5 + '');
+ port_plating.setAttributeNS(null, 'width', (output_position_end - output_position_start) + '');
+ port_plating.setAttributeNS(null, 'height', output_height/1.5 + '');
+
+ }
+ else {
+ output_x_position += output_port_size;
+ }
+
+ let type_class = 'unknown_type';
+ if (output.type) {
+ type_class = output.type + '_port';
+ }
+
+ // Draw the output port
+ const port_x_center = (output_position_start + output_position_end) / 2;
+ const port_y_center = nodeBox.height;
+
+ port_external.setAttributeNS(null, 'class', 'output external_port ' + type_class);
+ port_external.setAttributeNS(null, 'cx', port_x_center + '');
+ port_external.setAttributeNS(null, 'cy', port_y_center + '');
+ port_external.setAttributeNS(null, 'r', OUTPUT_PORT_REAL_SIZE + '');
+
+ port_internal.setAttributeNS(null, 'class', 'output internal_port');
+ port_internal.setAttributeNS(null, 'cx', port_x_center + '');
+ port_internal.setAttributeNS(null, 'cy', port_y_center + '');
+ port_internal.setAttributeNS(null, 'r', output_port_internal_size + '');
+
+ if (this.options.on_io_selected) {
+ const element_index = output_index; // Capture for use in callback
+ out_group.onclick = ((_ev: MouseEvent) => {
+ this.options.on_io_selected(this, 'out', element_index, output,
+ { x: port_x_center, y: port_y_center });
+ });
+ }
+ this.output_groups[output_index] = out_group;
+ }
+ }
+
+ private _renderInputs(group: SVGGElement) {
+ const nodeBox = group.getBoundingClientRect();
+
+ // Add inputs
+ let input_index = -1;
+
+ const input_initial_x_position = 10; // px
+ const inputs_x_margin = 10; // px
+ const input_plating_x_margin = 3; // px
+
+ let input_x_position = input_initial_x_position;
+
+ for (const input of this.options.inputs) {
+ input_index++;
+
+ const in_group = document.createElementNS(SvgNS, 'g');
+ in_group.classList.add('input');
+ group.appendChild(in_group);
+
+ const port_external = document.createElementNS(SvgNS, 'circle');
+ const port_internal = document.createElementNS(SvgNS, 'circle');
+
+ in_group.appendChild(port_external);
+ in_group.appendChild(port_internal);
+
+ const input_port_size = 50;
+ const input_port_internal_size = 5;
+ const input_position_start = input_x_position;
+ let input_position_end = input_x_position + input_port_size;
+
+
+ if (input.name) {
+ // Bind input name and port
+ const port_plating = document.createElementNS(SvgNS, 'rect');
+ in_group.appendChild(port_plating);
+
+ const text = document.createElementNS(SvgNS, 'text');
+ text.textContent = input.name;
+ text.setAttributeNS(null, 'class', 'argument_name input');
+ in_group.appendChild(text);
+
+ input_position_end = Math.max(input_position_end, (input_x_position
+ + text.getBoundingClientRect().width
+ + input_plating_x_margin * 2));
+
+ const input_width = input_position_end - input_position_start;
+
+ text.setAttributeNS(null, 'x', input_position_start + input_width/2 - text.getBoundingClientRect().width/2 + '');
+ text.setAttributeNS(null, 'y', nodeBox.height - (INPUT_PORT_REAL_SIZE/2) + '' );
+
+ input_x_position = input_position_end + inputs_x_margin;
+
+ const input_height = Math.max(input_port_size / 2, (INPUT_PORT_REAL_SIZE
+ + text.getBoundingClientRect().height));
+
+ // Configure port connector now that we know where the input will be positioned
+ port_plating.setAttributeNS(null, 'class', 'port_plating');
+ port_plating.setAttributeNS(null, 'x', input_position_start + '');
+ port_plating.setAttributeNS(null, 'y', nodeBox.height - input_height/1.5 + '');
+ port_plating.setAttributeNS(null, 'width', (input_position_end - input_position_start) + '');
+ port_plating.setAttributeNS(null, 'height', input_height/1.5 + '');
+
+ }
+ else {
+ input_x_position += input_port_size;
+ }
+
+ let type_class = 'unknown_type';
+ if (input.type) {
+ type_class = input.type + '_port';
+ }
+
+ // Draw the input port
+ const port_x_center = (input_position_start + input_position_end) / 2;
+ const port_y_center = 0;
+
+ port_external.setAttributeNS(null, 'class', 'input external_port ' + type_class);
+ port_external.setAttributeNS(null, 'cx', port_x_center + '');
+ port_external.setAttributeNS(null, 'cy', port_y_center + '');
+ port_external.setAttributeNS(null, 'r', INPUT_PORT_REAL_SIZE + '');
+
+ port_internal.setAttributeNS(null, 'class', 'input internal_port');
+ port_internal.setAttributeNS(null, 'cx', port_x_center + '');
+ port_internal.setAttributeNS(null, 'cy', port_y_center + '');
+ port_internal.setAttributeNS(null, 'r', input_port_internal_size + '');
+
+ if (this.options.on_io_selected) {
+ const element_index = input_index; // Capture for use in callback
+ in_group.onclick = ((_ev: MouseEvent) => {
+ this.options.on_io_selected(this, 'in', element_index, input,
+ { x: port_x_center, y: port_y_center });
+ });
+ }
+ this.input_groups[input_index] = in_group;
+ }
+ }
+
+ public static GetBlockType(): string {
+ return BLOCK_TYPE;
+ }
+
+ serialize(): FlowBlockData {
+ return {
+ type: BLOCK_TYPE,
+ value: {
+ options: JSON.parse(JSON.stringify(this.options)),
+ extra: JSON.parse(JSON.stringify(this.blockData)),
+ },
+ }
+ }
+
+ public static Deserialize(data: UiFlowBlockData, blockId: string, manager: BlockManager, toolbox: Toolbox): FlowBlock {
+ if (data.type !== BLOCK_TYPE){
+ throw new Error(`Block type mismatch, expected ${BLOCK_TYPE} found: ${data.type}`);
+ }
+
+ const options: UiFlowBlockOptions = JSON.parse(JSON.stringify(data.value.options));
+ options.on_dropdown_extended = manager.onDropdownExtended.bind(manager);
+ options.on_inputs_changed = manager.onInputsChanged.bind(manager);
+ options.on_io_selected = manager.onIoSelected.bind(manager);
+
+ const templateOptions = this._findTemplateOptions(options.id, toolbox);
+ options.builder = templateOptions.builder;
+
+ const block = new UiFlowBlock(options, blockId, toolbox.uiSignalService);
+
+ if (data.value.extra) {
+ block.blockData = JSON.parse(JSON.stringify(data.value.extra));
+ }
+ else {
+ block.blockData = {};
+ }
+
+ return block;
+ }
+
+ protected static _findTemplateOptions(blockId: string, toolbox: Toolbox): UiFlowBlockOptions {
+ for (const block of toolbox.blocks) {
+ if ((block as UiFlowBlockOptions).id === blockId) {
+ // This is done in this order to detect the cases where the ID
+ // matches but the type doesn't (the `else`)
+ if (isUiFlowBlockOptions(block)) {
+ return block;
+ }
+ else {
+ throw new Error(`BlockId found with different type (type: ${(block as any).type}).`);
+ }
+ }
+ }
+
+ throw new Error(`Renderer not found for block (id: ${blockId}).`);
+ }
+
+ public getBodyElement(): SVGGraphicsElement {
+ return this.group;
+ }
+
+ public getBodyArea(): Area2D {
+ const rect = this.handler.getBodyElement().getBBox();
+ return {
+ x: this.position.x,
+ y: this.position.y,
+ width: rect.width,
+ height: rect.height,
+ }
+ }
+
+ public getOffset(): Position2D {
+ return {x: this.position.x, y: this.position.y};
+ }
+
+ public moveTo(pos: Position2D) {
+ this.position.x = pos.x;
+ this.position.y = pos.y;
+
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+ }
+
+ public moveBy(distance: {x: number, y: number}): FlowBlock[] {
+ if (!this.group) {
+ throw Error("Not rendered");
+ }
+
+ this.position.x += distance.x;
+ this.position.y += distance.y;
+ this.group.setAttribute('transform', `translate(${this.position.x}, ${this.position.y})`)
+
+ if(!this._updating) {
+ for (const callback of this.onMoveCallbacks) {
+ callback(this.position);
+ }
+ }
+
+ return [];
+ }
+
+ public onMove(callback: (pos: Position2D) => void) {
+ this.onMoveCallbacks.push(callback);
+ }
+
+ public endMove(): FlowBlock[] {
+ if (!this.handler.dropOnEndMove) {
+ return [];
+ }
+
+ const movement = this.handler.dropOnEndMove();
+ return this.moveBy(movement);
+ }
+
+ onGetFocus() {
+ if (this.handler.onGetFocus) {
+ this.handler.onGetFocus();
+ }
+ }
+
+ onLoseFocus() {
+ if (this.handler.onLoseFocus) {
+ this.handler.onLoseFocus();
+ }
+ }
+
+ addConnection(direction: "in" | "out", input_index: number, block: FlowBlock): boolean {
+ if (direction === 'out') { return false; }
+
+ this.input_blocks.push([block, input_index]);
+ this.handler.onInputUpdated(block, input_index);
+
+ if (!this.input_count[input_index]) {
+ this.input_count[input_index] = 0;
+ }
+ this.input_count[input_index]++;
+
+ const extra_opts = this.options.extra_inputs;
+ if (!extra_opts) {
+ return;
+ }
+
+ // Consider need for extra inputs
+ let has_available_inputs = false;
+ for (let i = 0; i < this.options.inputs.length; i++) {
+ if (!this.input_count[i]) {
+ has_available_inputs = true;
+ break;
+ }
+ }
+
+ if (has_available_inputs) {
+ // Available inputs, nothing to do
+ return;
+ }
+
+
+ if ((extra_opts.quantity === 'any')
+ || (extra_opts.quantity.max < this.input_groups.length)) {
+
+ throw new Error("Generation of extra inputs not implemented.");
+ }
+
+ return false;
+ }
+
+ removeConnection(direction: "in" | "out", portIndex: number, block: FlowBlock): boolean {
+ if (direction === 'out') { return false; }
+
+ if (this.input_count[portIndex]) {
+ this.input_count[portIndex]--;
+ }
+
+ const index = this.input_blocks.findIndex(([x, y]) => x === block);
+
+ this.input_blocks.splice(index, 1);
+ this.handler.onConnectionLost(portIndex);
+
+ return false;
+ }
+
+ updateConnectionValue(block: FlowBlock, value: string) {
+ const input = this.input_blocks.find(([x, y]) => x === block);
+ let input_index = -1;
+ if (input) {
+ input_index = input[1];
+ }
+
+ this.handler.onConnectionValueUpdate(input_index, value);
+ }
+
+ public getBlockContextActions(): BlockContextAction[] {
+ const actions: BlockContextAction[] = [];
+
+ const handler = this.handler;
+ if (handler.isTextEditable()) {
+ actions.push({
+ title: 'Edit ' + handler.editableTextName,
+ run: this.startEditing.bind(this)
+ });
+ }
+
+ return actions;
+ }
+
+ startEditing() {
+ const handler = this.handler;
+ if (!handler.isTextEditable()) {
+ throw new Error("Not editable");
+ }
+
+ const prevValue = handler.text;
+
+ const area = handler.getArea();
+ const offset = this.getOffset();
+
+ area.x += offset.x;
+ area.y += offset.y;
+ this.group.classList.add('editing');
+
+ this._workspace.editInline(area, prevValue, 'string', (newValue: string) => {
+ this.group.classList.remove('editing');
+
+ if (newValue.trim().length > 0) {
+ handler.text = newValue.trim();
+ }
+
+ this.notifyOptionsChange();
+ });
+ }
+
+
+ private _updating: boolean = false;
+ public updateOptions(blockData: FlowBlockData): void {
+ const data = blockData as UiFlowBlockData;
+ this.blockData = data.value.extra;
+
+ const wasUpdating = this._updating;
+ this._updating = true;
+ try {
+ this.handler.updateOptions();
+ }
+ finally {
+ this._updating = wasUpdating;
+ }
+ }
+
+ notifyOptionsChange() {
+ if (this._workspace) {
+ this._workspace.onBlockOptionsChanged(this);
+ }
+ }
+
+ getSlots(): { [key: string]: string; } {
+ return {};
+ }
+
+ getInputs(): InputPortDefinition[] {
+ return JSON.parse(JSON.stringify(this.options.inputs));
+ }
+
+ getPositionOfInput(index: number, edge?: boolean): Position2D {
+ const group = this.input_groups[index];
+ const circle = group.getElementsByTagName('circle')[0];
+ const position = { x: parseInt(circle.getAttributeNS(null, 'cx')),
+ y: parseInt(circle.getAttributeNS(null, 'cy')),
+ };
+
+ if (edge) {
+ position.y += INPUT_PORT_REAL_SIZE;
+ }
+
+ return position;
+ }
+
+ getPositionOfOutput(index: number, edge?: boolean): Position2D {
+ const group = this.output_groups[index];
+ const circle = group.getElementsByTagName('circle')[0];
+ const position = { x: parseInt(circle.getAttributeNS(null, 'cx')),
+ y: parseInt(circle.getAttributeNS(null, 'cy')),
+ };
+
+ if (edge) {
+ position.y += OUTPUT_PORT_REAL_SIZE;
+ }
+
+ return position;
+ }
+
+ getOutputType(index: number): string {
+ return this.options.outputs[index].type;
+ }
+
+ public getInputType(index: number): string {
+ return this.options.inputs[index].type;
+ }
+
+ getOutputRunwayDirection(): Direction2D {
+ return 'down';
+ }
+
+ // Configurable handlers
+ get workspace(): FlowWorkspace {
+ return this._workspace;
+ }
+
+ // UI Flow block specific
+ onclick() {
+ this.handler.onClick();
+ }
+
+ isAutoresizable(): this is Autoresizable {
+ if (!this.handler.isAutoresizable) {
+ return false;
+ }
+
+ return this.handler.isAutoresizable();
+ }
+
+ isHorizontallyStackable(): boolean {
+ return !this.handler.isNotHorizontallyStackable;
+ }
+
+ getMinSize(): { width: number, height: number } {
+ if (!this.handler.isAutoresizable || !this.handler.isAutoresizable()) {
+ throw Error("Not autoresizable")
+ }
+
+ return this.handler.getMinSize();
+ }
+
+
+ doesTakeAllHorizontal(): boolean {
+ if (!this.handler.isAutoresizable || !this.handler.isAutoresizable()) {
+ throw Error("Not autoresizable")
+ }
+
+ return this.handler.doesTakeAllHorizontal();
+ }
+
+ doesTakeAllVertical(): boolean {
+ if (!this.handler.isAutoresizable || !this.handler.isAutoresizable()) {
+ throw Error("Not autoresizable")
+ }
+
+ return this.handler.doesTakeAllVertical();
+ }
+
+ // Container-related
+ updateContainer(container: FlowBlock) {
+ if (this.handler.updateContainer) {
+ this.handler.updateContainer(container as (UiFlowBlock | null));
+
+ }
+ this._container = container as (UiFlowBlock | null);
+ }
+
+
+ hasAncestor(block: FlowBlock): boolean {
+ if (!this._container) {
+ return false;
+ }
+ if (this._container === block) {
+ return true;
+ }
+ return this._container.hasAncestor(block);
+ }
+}
diff --git a/frontend/src/app/flow-editor/ui-blocks/ui_toolbox_description.ts b/frontend/src/app/flow-editor/ui-blocks/ui_toolbox_description.ts
new file mode 100644
index 00000000..d3dd8e1d
--- /dev/null
+++ b/frontend/src/app/flow-editor/ui-blocks/ui_toolbox_description.ts
@@ -0,0 +1,115 @@
+import { ToolboxDescription } from '../base_toolbox_description';
+import { UI_ICON } from '../definitions';
+import { ResponsivePageBuilder, ResponsivePageGenerateTree } from './renderers/responsive_page';
+import { SimpleButtonBuilder } from './renderers/simple_button';
+import { HorizontalUiSectionBuilder, HorizontalUiSectionGenerateTree } from './renderers/horizontal_ui_section';
+import { FixedTextBuilder } from './renderers/fixed_text';
+import { DynamicTextBuilder } from './renderers/dynamic_text';
+import { FixedImageBuilder } from './renderers/fixed_image';
+import { HorizontalSeparatorBuilder } from './renderers/horizontal_separator';
+import { SimpleUiCardBuilder, SimpleUiCardGenerateTree } from './renderers/simple_ui_card';
+import { LinkAreaBuilder, LinkAreaGenerateTree } from './renderers/link_area';
+import { TextBoxBuilder } from './renderers/text_box';
+
+export const UiToolboxDescription: ToolboxDescription = [
+ {
+ id: 'basic_UI',
+ name: 'Basic UI',
+ blocks: [
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ id: 'simple_button',
+ builder: SimpleButtonBuilder,
+ outputs: [
+ {
+ type: "user-pulse",
+ },
+ {
+ name: "button text",
+ type: "string",
+ }
+ ]
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ id: 'text_box',
+ builder: TextBoxBuilder,
+ outputs: [
+ {
+ name: "On change",
+ type: "user-pulse",
+ },
+ {
+ name: "Contents",
+ type: "string",
+ }
+ ]
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ id: 'fixed_text',
+ builder: FixedTextBuilder,
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ id: 'dynamic_text',
+ builder: DynamicTextBuilder,
+ inputs: [
+ {
+ type: "any",
+ },
+ ]
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ id: 'horizontal_separator',
+ builder: HorizontalSeparatorBuilder,
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ id: 'fixed_image',
+ builder: FixedImageBuilder,
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ subtype: 'container_flow_block',
+ id: 'responsive_page_holder',
+ builder: ResponsivePageBuilder,
+ gen_tree: ResponsivePageGenerateTree,
+ isPage: true,
+ is_internal: true,
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ subtype: 'container_flow_block',
+ id: 'horizontal_ui_section',
+ builder: HorizontalUiSectionBuilder,
+ gen_tree: HorizontalUiSectionGenerateTree,
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ subtype: 'container_flow_block',
+ id: 'simple_card',
+ builder: SimpleUiCardBuilder,
+ gen_tree: SimpleUiCardGenerateTree,
+ },
+ {
+ icon: UI_ICON,
+ type: 'ui_flow_block',
+ subtype: 'container_flow_block',
+ id: 'link_area',
+ builder: LinkAreaBuilder,
+ gen_tree: LinkAreaGenerateTree,
+ },
+ ]
+ }
+];
diff --git a/frontend/src/app/flow-editor/utils.ts b/frontend/src/app/flow-editor/utils.ts
new file mode 100644
index 00000000..a1623524
--- /dev/null
+++ b/frontend/src/app/flow-editor/utils.ts
@@ -0,0 +1,37 @@
+import { Area2D } from "./flow_block";
+
+export function uuidv4() {
+ // From https://stackoverflow.com/a/2117523
+ // Used to generate unique-in-svg IDs for blocks in workspace
+ // It just has to be reasonably unique, impredictability here is just overhead.
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+}
+
+export function isContainedIn(contained: Area2D, container: Area2D): boolean {
+ const diffX = contained.x - container.x ;
+ const diffY = contained.y - container.y;
+
+ return ((diffX >= 0)
+ && (diffY >= 0)
+ && ((container.width - diffX) >= contained.width)
+ && ((container.height - diffY) >= contained.height)
+ );
+}
+
+export function maxKey(list: T[], key: (el: T) => number): T | null {
+ let max = null;
+ let maxEl = null;
+ for (const el of list) {
+ const num = key(el);
+
+ if ((max == null) || (num > max)) {
+ max = num;
+ maxEl = el;
+ }
+ }
+
+ return maxEl;
+}
diff --git a/frontend/src/app/group.service.ts b/frontend/src/app/group.service.ts
new file mode 100644
index 00000000..09ae73aa
--- /dev/null
+++ b/frontend/src/app/group.service.ts
@@ -0,0 +1,221 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Collaborator, CollaboratorRole } from 'app/types/collaborator';
+import { ContentType } from './content-type';
+import { GroupInfo, UserGroupInfo } from './group';
+import { SessionService } from './session.service';
+import { SharedResource } from './bridges/bridge';
+import { EnvironmentService } from './environment.service';
+import { ground } from './utils';
+
+export interface UserAutocompleteInfo {
+ id: string,
+ username: string,
+};
+
+@Injectable()
+export class GroupService {
+ constructor(
+ private http: HttpClient,
+ private sessionService: SessionService,
+ private environmentService: EnvironmentService,
+ ) {
+ this.http = http;
+ this.sessionService = sessionService;
+ }
+
+ getUserAutocompleteUrl(): string {
+ return `${this.environmentService.getApiRoot()}/utils/autocomplete/users`;
+ }
+
+ getCreateGroupUrl(): string {
+ return `${this.environmentService.getApiRoot()}/groups`;
+ }
+
+ getGroupInfoUrl(groupName: string): string {
+ return `${this.environmentService.getApiRoot()}/groups/by-name/${groupName}`;
+ }
+
+ getGroupCollaboratorsUrl(groupId: string): string {
+ return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}/collaborators`;
+ }
+
+ updateGroupCollaboratorsUrl(groupId: string): string {
+ return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}/collaborators`;
+ }
+
+ getUpdateGroupAvatarUrl(groupId: string): string {
+ return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}/picture`;
+ }
+
+ getUpdateGroupUrl(groupId: string): string {
+ return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}`;
+ }
+
+ getDeleteGroupUrl(groupId: string): string {
+ return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}`;
+ }
+
+ getGroupSharedResourcesUrl(groupId: string): string {
+ return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}/shared-resources`;
+ }
+
+ async getUserGroupsUrl(): Promise {
+ const root = await this.sessionService.getApiRootForUserId()
+ return `${root}/groups`;
+ }
+
+ async autocompleteUsers(query: string): Promise {
+ const url = this.getUserAutocompleteUrl();
+
+ const result = await this.http.get(url, {
+ headers: this.sessionService.getAuthHeader(),
+ params: { q: query },
+ }).toPromise();
+
+ return (result as any)['users'];
+ }
+
+ async createGroup(name: string, options: { 'public': boolean, collaborators: { id: string, role: CollaboratorRole }[] } ): Promise {
+ const url = this.getCreateGroupUrl();
+
+ const result = await this.http.post(url,
+ JSON.stringify({
+ name: name,
+ 'public': options['public'],
+ collaborators: options.collaborators,
+ }),
+ {
+ headers: this.sessionService.addContentType(
+ this.sessionService.getAuthHeader(),
+ ContentType.Json)
+ }).toPromise();
+
+ return (result as any)['group'];
+ }
+
+ async getUserGroups(): Promise {
+ const url = await this.getUserGroupsUrl();
+
+ const result = await this.http.get(url, { headers: this.sessionService.getAuthHeader()})
+ .toPromise();
+
+ return (result as any)['groups'].map((group: GroupInfo) => ground(this.environmentService, group, 'picture'));
+ }
+
+ async getGroupWithName(groupName: any): Promise {
+ const url = this.getGroupInfoUrl(groupName);
+
+ const result = await (this.http.get(url, { headers: this.sessionService.getAuthHeader()}).toPromise());
+
+ return (result as any)['group'];
+ }
+
+ async getCollaboratorsOnGroup(groupId: string): Promise {
+ const url = this.getGroupCollaboratorsUrl(groupId);
+
+ const result = await this.http.get(url, { headers: this.sessionService.getAuthHeader()})
+ .toPromise();
+
+ return (result as any)['collaborators'].map((collaborator: Collaborator) => ground(this.environmentService, collaborator, 'picture'));
+ }
+
+ async inviteUsers(groupId: string, userIds: { id: string, role: CollaboratorRole }[]): Promise {
+ const url = this.updateGroupCollaboratorsUrl(groupId);
+
+ await (this.http
+ .post(url,
+ JSON.stringify({
+ action: 'invite',
+ collaborators: userIds.map(user => {
+ return {
+ id: user.id,
+ role: user.role,
+ }}),
+ }),
+ {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(),
+ ContentType.Json)})
+ .toPromise());
+ }
+
+ async updateGroupCollaboratorList(groupId: string, userIds: { id: string, role: CollaboratorRole }[]): Promise {
+ const url = this.updateGroupCollaboratorsUrl(groupId);
+ const operatorId = (await this.sessionService.getSession()).user_id;
+
+ const safeCollaborators: { id: string, role: CollaboratorRole }[] = (userIds
+ .map((user) => {
+ return {
+ id: user.id,
+ role: user.role,
+ }})
+ .filter((user) => user.id !== operatorId));
+ // Make sure that the operator will always be left as admin to be able
+ // to correct potential configuration errors
+ safeCollaborators.push({ id: operatorId, role: 'admin'});
+
+ await (this.http
+ .post(url,
+ JSON.stringify({
+ action: 'update',
+ collaborators: safeCollaborators
+ }),
+ {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(),
+ ContentType.Json)})
+ .toPromise());
+ }
+
+ async updateGroupAvatar(groupId: string, image: File): Promise {
+ const formData = new FormData();
+ formData.append('file', image);
+
+ const url = this.getUpdateGroupAvatarUrl(groupId);
+
+ await this.http.post(url, formData, { headers: this.sessionService.getAuthHeader() }).toPromise()
+ }
+
+ async setPublicStatus(groupId: string, newStatus: boolean): Promise {
+ const url = this.getUpdateGroupUrl(groupId);
+
+ await (this.http
+ .patch(url,
+ JSON.stringify({
+ 'public': newStatus
+ }),
+ {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(),
+ ContentType.Json)})
+ .toPromise());
+ }
+
+ async updateMinLevelForPrivateBridgeUsage(groupId: string, minLevel: CollaboratorRole | 'not_allowed') {
+ const url = this.getUpdateGroupUrl(groupId);
+
+ await (this.http
+ .patch(url,
+ JSON.stringify({
+ 'min_level_for_private_bridge_usage': minLevel
+ }),
+ {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(),
+ ContentType.Json)})
+ .toPromise());
+ }
+
+ async deleteGroup(groupId: string): Promise {
+ const url = this.getDeleteGroupUrl(groupId);
+
+ await (this.http
+ .delete(url,
+ {headers: this.sessionService.getAuthHeader()})
+ .toPromise());
+ }
+
+ async getSharedResources(groupId: string): Promise {
+ const url = this.getGroupSharedResourcesUrl(groupId);
+
+ const response = await (this.http
+ .get(url,
+ {headers: this.sessionService.getAuthHeader()})
+ .toPromise());
+
+ return (response as any)['resources'] as SharedResource[];
+ }
+}
diff --git a/frontend/src/app/group.ts b/frontend/src/app/group.ts
new file mode 100644
index 00000000..8cd6a377
--- /dev/null
+++ b/frontend/src/app/group.ts
@@ -0,0 +1,14 @@
+import { CollaboratorRole } from "./types/collaborator";
+
+export interface GroupInfo {
+ name: string,
+ canonical_name: string,
+ id: string,
+ picture?: string,
+ 'public': boolean,
+ min_level_for_private_bridge_usage: CollaboratorRole | 'not_allowed',
+};
+
+export interface UserGroupInfo extends GroupInfo {
+ role: CollaboratorRole,
+};
diff --git a/frontend/src/app/how-to-enable-service-dialog.css b/frontend/src/app/how-to-enable-service-dialog.css
deleted file mode 100644
index 9f341228..00000000
--- a/frontend/src/app/how-to-enable-service-dialog.css
+++ /dev/null
@@ -1,13 +0,0 @@
-div.message-example {
- padding: 1ex;
- border-radius: 2px;
- background-color: #333;
- margin-top: 1ex;
- margin-bottom: 1ex;
- color: #eee;
-}
-
-button.confirmation {
- right: 0;
- margin-left: auto;
-}
\ No newline at end of file
diff --git a/frontend/src/app/how-to-enable-service-dialog.html b/frontend/src/app/how-to-enable-service-dialog.html
deleted file mode 100644
index 6e144baa..00000000
--- a/frontend/src/app/how-to-enable-service-dialog.html
+++ /dev/null
@@ -1,11 +0,0 @@
-How to enable service
-
-
-
-
- OK
-
-
- Cancel
- Submit
-
diff --git a/frontend/src/app/info-pages/about-page.component.css b/frontend/src/app/info-pages/about-page.component.css
index d91790d4..67f4b2e5 100644
--- a/frontend/src/app/info-pages/about-page.component.css
+++ b/frontend/src/app/info-pages/about-page.component.css
@@ -6,14 +6,18 @@
font-weight: 800;
font-size: 80px;
- color: #444;
+ color: #3b3b3b;
text-shadow: 0px 0px 1px #000;
}
+#project-name > p > .logo {
+ max-height: 30vh;
+}
+
#project-offer {
text-align: center;
padding: 2ex;
- color: #444;
+ color: #3b3b3b;
}
#project-claim {
diff --git a/frontend/src/app/info-pages/about-page.component.html b/frontend/src/app/info-pages/about-page.component.html
index 5548ba63..f7f36747 100644
--- a/frontend/src/app/info-pages/about-page.component.html
+++ b/frontend/src/app/info-pages/about-page.component.html
@@ -1,59 +1,65 @@
-
-
- PrograMaker
-
+
+
+
+
+
+
+ PrograMaker
+
+
-
+
-
Build your own internet
+
Your things, your rules
-
+
-
PrograMaker is an Open Source platform to program services and devices using visual tools.
+
PrograMaker is an Open Source platform to easily program services and devices using visual tools.
-
+
-
-
+
- Try it !
-
-
+ Try it !
+
+
-
+
-
-
-
-
Connect devices to databases
-
-
+
+
+
+
Connect devices to databases
+
+
-
-
Add logic to existing services
-
+
+
Add logic to existing services
+
+
-
-
+
-
+
-
-
-
-
Inspirations
-
-
- IFTTT
-
- is a great tool to connect together different services in simple flows.
- PrograMaker expands on this concept to allow more flexible usage of possibilities from services and devices, and adds the possibility to add additional logic and memory to these flows. All of this without having to write traditional code.
-
-
-
+
+
+
+
Inspirations
+
+
+ IFTTT
+
+ is a great tool to connect together different services in simple flows.
+ PrograMaker expands on this concept to allow more flexible usage of possibilities from services and devices, and adds the possibility to add additional logic and memory to these flows. All of this without having to write traditional code.
+
+
+
+
diff --git a/frontend/src/app/info-pages/about-page.component.ts b/frontend/src/app/info-pages/about-page.component.ts
index 40de3eff..fb88ffdf 100644
--- a/frontend/src/app/info-pages/about-page.component.ts
+++ b/frontend/src/app/info-pages/about-page.component.ts
@@ -1,8 +1,8 @@
-import { Component, OnInit } from '@angular/core';
-import { Router } from '@angular/router';
+import { HttpClient } from '@angular/common/http';
+import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { environment } from 'environments/environment';
import { SessionService } from '../session.service';
-import { environment } from '../../environments/environment';
-
@Component({
selector: 'app-about-page',
@@ -12,20 +12,26 @@ import { environment } from '../../environments/environment';
'../libs/css/material-icons.css',
'../libs/css/bootstrap.min.css',
],
- providers: [SessionService]
+ providers: [SessionService, HttpClient]
})
-
-export class AboutPageComponent implements OnInit {
+export class AboutPageComponent implements AfterViewInit {
environment: { [key: string]: any };
+ @ViewChild('container') container: ElementRef
;
constructor (
private router: Router,
+ private route: ActivatedRoute,
) {
- this.router = router;
+ this.environment = environment;
}
- ngOnInit(): void {
- this.environment = environment;
+ ngAfterViewInit(): void {
+ this.route.data
+ .subscribe((data: {renderedAbout: string }) => {
+ if (data.renderedAbout) {
+ this.container.nativeElement.innerHTML = data.renderedAbout;
+ }
+ });
}
followCallToAction() {
diff --git a/frontend/src/app/info-pages/home-redirect.component.ts b/frontend/src/app/info-pages/home-redirect.component.ts
index db61cbe6..d01d69cf 100644
--- a/frontend/src/app/info-pages/home-redirect.component.ts
+++ b/frontend/src/app/info-pages/home-redirect.component.ts
@@ -22,13 +22,13 @@ export class HomeRedirectComponent implements OnInit {
this.sessionService.getSession()
.then(session => {
if (session.active) {
- this.router.navigate(['/dashboard']);
+ this.router.navigate(['/dashboard'], {replaceUrl:true});
}
else {
- this.router.navigate(['/about']);
+ this.router.navigate(['/about'], {replaceUrl:true});
}
}).catch(_err => {
- this.router.navigate(['/about']);
+ this.router.navigate(['/about'], {replaceUrl:true});
});
}
}
diff --git a/frontend/src/app/json.ts b/frontend/src/app/json.ts
index 9e17edfc..cdea38c1 100644
--- a/frontend/src/app/json.ts
+++ b/frontend/src/app/json.ts
@@ -7,7 +7,7 @@ enum JSONType {
Map,
}
-const GetTypeOfJson = (function(element): JSONType {
+const GetTypeOfJson = (function(element: any): JSONType {
if (element === null) {
return JSONType.Null;
} else if (typeof element === 'boolean') {
diff --git a/frontend/src/app/login-form/login-form.component.html b/frontend/src/app/login-form/login-form.component.html
index 71e2578c..2feb6974 100644
--- a/frontend/src/app/login-form/login-form.component.html
+++ b/frontend/src/app/login-form/login-form.component.html
@@ -1,50 +1,52 @@
-
-