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) -> + [ <<"
">> + ]; + +render_element(E=#{ <<"widget_type">> := <<"fixed_text">> + , <<"id">> := WidgetId + }, _ProgramId, _Values, _Req) -> + ElementStyle = get_text_element_style(E), + [ <<"
> + , " style='", ElementStyle, "'" + , <<">">> + , get_fixed_text_content(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), + [ <<"
> + , " style='", ElementStyle, "'" + , <<">
">> + , Contents + , <<"
">> + ]; + + +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) + , "" + ]. + +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}}
- - - - - 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. +
+
+
+ +
+
Create new program
+
+
+
+ +
+ + +
+
+ + + +
+
{{program.name}}
+
+ + + + {{ bridgeInfo[bridgeId].name }} + + +
+
+
+
+
Archive {{ program.name }}?
+
The program will stop until you re-start it.
+ +
+
+ +
+ +
+
+
+
+
+
+ + + Archived programs + + +
+ These are programs that you have archived, they won't be running until you launch them again. +
+
+ +
+ +
+
+ + + +
+
{{program.name}}
+
+ + + + {{ bridgeInfo[bridgeId].name }} + + +
+
+
+ +
+
+
+
+
+
+ + + 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 }} + +
+ {{ 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 }} + +
+ {{bridge.name || "unnamed bridge"}} +
+
+
+
+
+
+
+
+ +
+
+ +
+ +
{{ profile.name }}
+ + + +
+
+ Groups + + +
+ +
+ You have not joined any group. +
+ +
+ + {{ group.name }} + + + {{ group.name }} + + +
+ +
+ +
+
+ Collaborators + + + +
+ +
+ This group has no public collaborator. +
+ +
+ + + {{ _roleToIcon(user.role) }} + + + {{ user.username }} + + + {{ 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
+
+ + +
+ + + + or + + + +
+
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 }}?

+ + + + + + public Public + + + link Share by link + + + lock Private + + + + + + + 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 }} + +
+ + +
+
+ + Connect to necessary bridges +
+ +
+ +
+ + This program doesn't use any bridge. You can go to the next step. + + + + +
+ +
+ + + +
+
+ + Name your new program + + + + +
+ + + +
+
+ + Summary and confirmation + +
    +
  • Cloning to: + + + My user + + + Group: {{ selectedGroup.name }} + + +
  • + +
  • Name: + + + + + +
  • + +
  • Linking bridges: + + No linking needed. + + + + +
  • +
+ +
+ + + + +
+
+
+
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! + + + + + 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 + + + + +
+ +
+

Select a bridge to connect to

+ + +
+
+
+ +
+ +

{{ bridge.name }}

+
+
+
+
+
+
+ + + +
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. +
+ + + +
+ + + or + + +
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 }} +

+ +
+

+ + + Bridge Info +

+ +
+
+ Connection URL: + {{ connectionUrl }} +
+ +
+
+ + + Connection tokens + + +
+ +
+
+ + + + + {{ saveTokenErrorMessage }} + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ Token name + + Token key + + +
{{ token.name }} + + + + + ************** + + + + + + Copy now the tooltip. Later it won't be possible to view it. + +
+
+
+
+
+ +
+

+ + + Resources +

+ +
+
+
{{ resource.name }}
+ +
    +
  • +
    + + + {{ entry.name }} + + +
    + +
      +
    • + + Shared with + + + group + + + {{ group.name }} + + + + +
    • +
    +
  • +
+
+ +
+ + You've prepared some changes for the resource sharing. + + +
+ + + +
+
+ +
+
+ +
+

+ + Signals +

+ +
+
+
+ + + Incoming Signals +
+ +
+
+
+ {{ _stringify(signal, null, 4) }} +
+
+
+
+ +
+
+ + Signal history +
+ +
+
+
+ + {{ conn.saving ? 'Log data' : "Don't log" }} + + + + + {{ conn.name }} + + +
+
+ +
+ + You've prepared some changes for the signal logging. + + +
+ + + +
+
+ +
+
+ {{ _stringify(signal, null, 4) }} +
+
+
+
+
+
+ +
+
+ + + +
+
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

+
+
+
+ Text color: +
+ +
+ +
+
+ Font size: px +
+ +
+ +
+
+ Boldness: +
+ + SuperLight + Light + Normal + Bold + SuperBold + +
+ +
+ +
+ The quick brown fox jumps over the lazy dog +
+
+
+
+ +
+

Element configuration

+
+
+
+ +
+ + +
+ +
+
+ Element width +
+ + {{ width }} + +
+ +
+
+
+
+
+ +
+
+
+ +
+

Element target

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

+ +
+
+
+ Text color: +
+ +
+ +
+ +
+
+
+ +
+ + + + + +
+ +
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 +

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

+ arrow_back_ios + {{program.name}} + Loading... +

+ + + + Scroll to find more buttons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+
+
+
+
+
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

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

- Project logo - PrograMaker -

+
+

+

+ +

+

+ Progra­Maker +

+

-
+
-

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 ! + +
-
+
-
-
-
-

Connect devices to databases

- Save noise lecture to DB showcase -
+
+
+
+

Connect devices to databases

+ Save noise lecture to DB showcase +
-
-

Add logic to existing services

- Schedule weather notification on week days +
+

Add logic to existing services

+ Schedule weather notification on week days +
-
-
+
-
+
+ Got any doubt? Ideas? Proposals? + Contact us on {{environment.contact_mail}} +
-
- -
-

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 @@ - +
+ +
+ Or + + Sign up + +
+ + +
diff --git a/frontend/src/app/login-form/login-form.component.ts b/frontend/src/app/login-form/login-form.component.ts index 60175002..d6d3e4bb 100644 --- a/frontend/src/app/login-form/login-form.component.ts +++ b/frontend/src/app/login-form/login-form.component.ts @@ -8,7 +8,10 @@ import { SessionService } from '../session.service'; @Component({ selector: 'app-my-login-form', templateUrl: './login-form.component.html', - providers: [SessionService] + providers: [SessionService], + styleUrls: [ + 'register-form.component.css', + ], }) export class LoginFormComponent implements OnInit { @@ -22,7 +25,7 @@ export class LoginFormComponent implements OnInit { .then(session => { this.session = session; if (session !== null && session.active) { - this.router.navigate(['/']); + this.router.navigate(['/'], {replaceUrl:true}); } }); } diff --git a/frontend/src/app/login-form/register-form.component.css b/frontend/src/app/login-form/register-form.component.css index 743861cd..6098bae2 100644 --- a/frontend/src/app/login-form/register-form.component.css +++ b/frontend/src/app/login-form/register-form.component.css @@ -1,4 +1,13 @@ +.container { + margin: auto; + padding: 1ex; +} + mat-error { padding-left: 2em; padding-bottom: 1em; } + +mat-card form { + padding: 0; +} diff --git a/frontend/src/app/login-form/register-form.component.html b/frontend/src/app/login-form/register-form.component.html index b69cb3c2..9a6251cf 100644 --- a/frontend/src/app/login-form/register-form.component.html +++ b/frontend/src/app/login-form/register-form.component.html @@ -1,88 +1,90 @@ - +
+
+ Or + + Log in + +
+ + +
diff --git a/frontend/src/app/login-form/register-form.component.ts b/frontend/src/app/login-form/register-form.component.ts index ec102475..53768316 100644 --- a/frontend/src/app/login-form/register-form.component.ts +++ b/frontend/src/app/login-form/register-form.component.ts @@ -38,7 +38,7 @@ export class RegisterFormComponent implements OnInit { .then(session => { this.session = session; if (session !== null && session.active) { - this.router.navigate(['/']); + this.router.navigate(['/'], {replaceUrl:true}); } }); } @@ -72,8 +72,16 @@ export class RegisterFormComponent implements OnInit { this.validUsername = true; this.userErrorMessage = ""; - if (this.username.length < 4) { - this.userErrorMessage = "User name should have at least 4 characters."; + if ((this.username.length < 4) || (this.username.length > 50)){ + this.userErrorMessage = "User name should have at more than 3 and at most 50 characters."; + this.validUsername = false; + } + else if (!this.username.match(/^[-_a-zA-Z0-9]*$/)) { + this.userErrorMessage = "User name can only contain letters (a-z), digits (0-9), underscores (_) and dashes (-)."; + this.validUsername = false; + } + else if (this.username.match(/^[-0-9]*$/)) { + this.userErrorMessage = "User name must container at least a letter (a-z)."; this.validUsername = false; } } @@ -125,9 +133,31 @@ export class RegisterFormComponent implements OnInit { this.router.navigate(['/register/wait_for_mail_verification']); } }) - .catch(e => { - console.log('Exception signing up', e); - this.errorMessage = "Error signing up"; + .catch(reason => { + this.errorMessage = 'Registration error'; + + if (reason.error && reason.error.error) { + if (reason.error.error.type === "invalid_username") { + this.errorMessage += ": Invalid username"; + } + else if (reason.error.error.type === "colliding_element") { + if (reason.error.error.subtype === "username") { + this.errorMessage += ": Repeated username"; + } + else if (reason.error.error.subtype === "email") { + this.errorMessage += ": Repeated email"; + } + else { + this.errorMessage += ": Repeated user data"; + } + } + else { + this.errorMessage += '. Error type: ' + reason.error.error.type; + } + } else if (reason.status === 400) { + this.errorMessage += ': Invalid user/password' + } + console.log('Registration error:', reason); }) } } diff --git a/frontend/src/app/login-form/register-wait-for-mail-verification.component.ts b/frontend/src/app/login-form/register-wait-for-mail-verification.component.ts index 900c9bd1..81b91508 100644 --- a/frontend/src/app/login-form/register-wait-for-mail-verification.component.ts +++ b/frontend/src/app/login-form/register-wait-for-mail-verification.component.ts @@ -19,7 +19,7 @@ export class WaitForMailVerificationComponent implements OnInit { .then(session => { this.session = session; if (session !== null && session.active) { - this.router.navigate(['/']); + this.router.navigate(['/'], {replaceUrl:true}); } }); } diff --git a/frontend/src/app/login-form/reset-password-start.component.ts b/frontend/src/app/login-form/reset-password-start.component.ts index fbaf9f94..bf47e099 100644 --- a/frontend/src/app/login-form/reset-password-start.component.ts +++ b/frontend/src/app/login-form/reset-password-start.component.ts @@ -28,7 +28,7 @@ export class ResetPasswordStartComponent implements OnInit { .then(session => { this.session = session; if (session !== null && session.active) { - this.router.navigate(['/']); + this.router.navigate(['/'], {replaceUrl:true}); } }); } diff --git a/frontend/src/app/login-form/reset-password-update-password.component.html b/frontend/src/app/login-form/reset-password-update-password.component.html index e7b6a707..273e0816 100644 --- a/frontend/src/app/login-form/reset-password-update-password.component.html +++ b/frontend/src/app/login-form/reset-password-update-password.component.html @@ -7,6 +7,9 @@
{{ infoMessage }}
+
+ Type and confirm your new password +
diff --git a/frontend/src/app/login-form/verify-code.component.ts b/frontend/src/app/login-form/verify-code.component.ts index 5d5fd167..cf47125c 100644 --- a/frontend/src/app/login-form/verify-code.component.ts +++ b/frontend/src/app/login-form/verify-code.component.ts @@ -31,7 +31,7 @@ export class VerifyCodeComponent implements OnInit { .subscribe(session => { console.log("Session:", session); if (session) { - this.router.navigate(['/']); + this.router.navigate(['/'], {replaceUrl:true}); } }); } diff --git a/frontend/src/app/monitor.service.ts b/frontend/src/app/monitor.service.ts index 9dddd3c7..7dfbc52a 100644 --- a/frontend/src/app/monitor.service.ts +++ b/frontend/src/app/monitor.service.ts @@ -1,19 +1,17 @@ - import {map} from 'rxjs/operators'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; - -import * as API from './api-config'; -import { ContentType } from './content-type'; import { MonitorMetadata } from './monitor'; import { SessionService } from './session.service'; +import { EnvironmentService } from './environment.service'; @Injectable() export class MonitorService { constructor( private http: HttpClient, private sessionService: SessionService, + private environmentService: EnvironmentService, ) { this.http = http; this.sessionService = sessionService; @@ -24,6 +22,10 @@ export class MonitorService { return userApiRoot + '/monitors/'; } + public async getListMonitorsOnProgramUrl(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/monitors`; + } + public async getRetrieveMonitorUrl(_user_id: string, Monitor_id: string) { const userApiRoot = await this.sessionService.getUserApiRoot(); return userApiRoot + '/monitors/' + Monitor_id; @@ -42,4 +44,14 @@ export class MonitorService { })) .toPromise()); } + + public async getMonitorsOnProgram(programId: string): Promise { + const url = await this.getListMonitorsOnProgramUrl(programId); + + return this.http.get(url, { headers: this.sessionService.getAuthHeader() }).pipe( + map((response) => { + return response as MonitorMetadata[]; + })) + .toPromise(); + } } diff --git a/frontend/src/app/new/group/new-group.component.css b/frontend/src/app/new/group/new-group.component.css new file mode 100644 index 00000000..2230b318 --- /dev/null +++ b/frontend/src/app/new/group/new-group.component.css @@ -0,0 +1,96 @@ +section.component { + max-width: 100%; + padding: 1ex; + width: max-content; + margin: 0 auto; +} + +.title { + font-size: 200%; +} + +.config-option .title { + font-size: 150%; + margin-bottom: 1ex; +} + +.intent { + margin-top: 1ex; +} + +.config-option .intent { + margin-bottom: 1ex; +} + +mat-form-field { + width: 40ex; + max-width: 100%; +} + +mat-error { + max-width: 40ex; +} + +button { + min-height: 5ex; + margin: 0 1ex 0 1ex; +} + +.explanation { + white-space: normal; +} + +.key-point { + text-decoration: underline; +} + +.config-option { + margin-top: 1em; +} + +.accept-cancel { + margin-top: 2em; +} + +.confirm-button { + background-color: #009688; + color: white; + font-weight: bold; +} + +mat-card { + margin-top: 1em; +} + +ul.collaborator-list { + padding: 0; + max-width: 90vw; +} + +li.collaborator { + display: inline-block; + background-color: #27212e; + margin: 0.5ex; + color: #fff; + font-size: small; + border-radius: 4px; +} + +button.remove-collaborator { + padding: 0; + margin: 0; + border-right: 1px solid rgba(0,0,0,0.5); + border: none; + background-color: rgba(255,255,255,0.3); + color: #fff; + border-radius: 4px 0 0 4px; + min-height: 3ex; +} + +button.remove-collaborator mat-icon { + vertical-align: bottom; +} + +li.collaborator .name { + margin: 0 0.5ex 0 0.5ex; +} diff --git a/frontend/src/app/new/group/new-group.component.html b/frontend/src/app/new/group/new-group.component.html new file mode 100644 index 00000000..0efba641 --- /dev/null +++ b/frontend/src/app/new/group/new-group.component.html @@ -0,0 +1,71 @@ +
+
Create a new group
+ + Let's create a new user group! We'll be done quickly. + +
+ {{ errorMessage }} +
+ + +
+
Group data
+ + + + + +
Group name is required
+
Group name requires at least {{ options.controls.groupName.errors.minlength.requiredLength }} characters
+
+ Group name can only contain letters (a-z, A-Z), numbers (0-9), whitespaces or underscores (_) +
+
+
+ +
+ + Public group (non-members can see the group) + Private group (only members can see the group) + +
+
+ + + +
+
Collaborators
+
+ Invite some people to the group (you can do this later). +
+ + +
+
+ +
+ + + or + + +
+
diff --git a/frontend/src/app/new/group/new-group.component.ts b/frontend/src/app/new/group/new-group.component.ts new file mode 100644 index 00000000..03c4bf41 --- /dev/null +++ b/frontend/src/app/new/group/new-group.component.ts @@ -0,0 +1,105 @@ +import { Location } from '@angular/common'; +import { Component, ViewChild } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { GroupCollaboratorEditorComponent } from 'app/components/group-collaborator-editor/group-collaborator-editor.component'; +import { GroupService } from 'app/group.service'; +import { BridgeService } from '../../bridges/bridge.service'; +import { ConnectionService } from '../../connection.service'; +import { MonitorService } from '../../monitor.service'; +import { ProgramService } from '../../program.service'; +import { ServiceService } from '../../service.service'; +import { Session } from '../../session'; +import { SessionService } from '../../session.service'; +import { canonicalizableValidator } from './new-group.component.validators'; +import { GroupInfo } from 'app/group'; + +@Component({ + // moduleId: module.id, + selector: 'app-my-new-group', + templateUrl: './new-group.component.html', + providers: [BridgeService, ConnectionService, GroupService, MonitorService, ProgramService, SessionService, ServiceService], + styleUrls: [ + 'new-group.component.css', + '../../libs/css/material-icons.css', + '../../libs/css/bootstrap.min.css', + ], +}) +export class NewGroupComponent { + session: Session; + is_advanced: boolean; + options: FormGroup; + publicGroup: boolean = false; + errorMessage: string = ''; + processing = false; + + @ViewChild('groupCollaboratorEditor') groupCollaboratorEditor: GroupCollaboratorEditorComponent; + + constructor( + private sessionService: SessionService, + private groupService: GroupService, + private router: Router, + private dialog: MatDialog, + private formBuilder: FormBuilder, + private _location: Location, + ) { + this.options = this.formBuilder.group({ + groupName: ['', [Validators.required, Validators.minLength(4), canonicalizableValidator()]], + }); + } + + // 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'], {replaceUrl:true}); + } + else { + this.is_advanced = this.session.tags.is_advanced; + } + }) + .catch(e => { + console.error('Error getting session', e); + this.router.navigate(['/login'], {replaceUrl:true}); + }); + } + + validateGroupName() { + } + + displayInvitation(user: {username: string}): string { + return user && user.username ? user.username : ''; + } + + createGroup() { + const groupName = this.options.controls.groupName.value; + const isPublicGroup = this.publicGroup; + const collaborators = this.groupCollaboratorEditor.getCollaborators(); + + this.processing = true; + this.groupService.createGroup(groupName, { + 'public': isPublicGroup, + collaborators: collaborators.map(user => { return { id: user.id, role: user.role }; }) + }) + .then((group: GroupInfo) => { + this.router.navigate(['/groups/' + group.name]); + }) + .catch(err => { + if ((err.name === 'HttpErrorResponse') && (err.status === 409)) { + this.errorMessage = 'A group already exists with this name.'; + // TODO: Properly integrate with group name validations + } + else { + console.error(err); + } + this.processing = false; + }); + } + + goBack() { + this._location.back(); + } +} diff --git a/frontend/src/app/new/group/new-group.component.validators.ts b/frontend/src/app/new/group/new-group.component.validators.ts new file mode 100644 index 00000000..c85f0fc6 --- /dev/null +++ b/frontend/src/app/new/group/new-group.component.validators.ts @@ -0,0 +1,10 @@ +import { AbstractControl, ValidatorFn } from '@angular/forms'; + +const canonicalizable = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "; + +export function canonicalizableValidator(): ValidatorFn { + return (control: AbstractControl): {[key: string]: any} | null => { + const error = (Array.from(control.value) as string[]).some(c => canonicalizable.indexOf(c) < 0); + return error ? {canonicalization: {value: control.value}} : null; + }; +} diff --git a/frontend/src/app/profiles/group-profile.component.html b/frontend/src/app/profiles/group-profile.component.html new file mode 100644 index 00000000..9af30f0f --- /dev/null +++ b/frontend/src/app/profiles/group-profile.component.html @@ -0,0 +1,70 @@ +
+
+
+
+ +
{{ profile.name }}
+
+
+
Collaborators
+ +
+
+ +
+
+ No public programs. +
+
+
+
Public Programs
+
+ +
+ + +
+
+ + +
+
{{program.name}}
+
+ + + + {{ bridgeInfo[bridgeId].name }} + + +
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/profiles/group-profile.component.scss b/frontend/src/app/profiles/group-profile.component.scss new file mode 100644 index 00000000..9901b20d --- /dev/null +++ b/frontend/src/app/profiles/group-profile.component.scss @@ -0,0 +1,521 @@ +.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; +} + +.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; + max-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.row { + margin-left: 0; + margin-right: 0; + + .user { + padding: 0; + } +} +.profile section { + width: 100%; + + &.groups { + width: max-content; + max-width: 100%; + min-width: 15ex; + + + .section-title { + padding: 0.25rem; + } + + .section-objects { + text-align: center; + } + } + + & > hr { + width: 12rem; + max-width: 100%; + background-color: #888; + } + + &.programs { + padding: 1rem 0.25rem 2rem 0.25rem; + .program-list, .empty-section-explanation { + background-color: #eee; + border-radius: 4px; + } + + .empty-section-explanation { + padding: 1rem; + font-weight: bold; + } + } +} + +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 .bridge-connections-subsection { + border-top: 1px solid #aaa; +} + +section.bridges .bridge-connections-subsection 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; +} diff --git a/frontend/src/app/profiles/group-profile.component.ts b/frontend/src/app/profiles/group-profile.component.ts new file mode 100644 index 00000000..71cb881f --- /dev/null +++ b/frontend/src/app/profiles/group-profile.component.ts @@ -0,0 +1,82 @@ +import { Component, ViewChild, Input } 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 { IconReference } from 'app/connection'; +import { ConnectionService } from 'app/connection.service'; +import { EnvironmentService } from 'app/environment.service'; +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 { ProgramMetadata } from '../program'; +import { ProgramService } from '../program.service'; +import { Session } from '../session'; +import { SessionService } from '../session.service'; +import { getGroupPictureUrl, getUserPictureUrl, iconDataToUrl } from '../utils'; +import { ProfileService, GroupProfileInfo } from './profile.service'; + + +@Component({ + // moduleId: module.id, + selector: 'app-my-group-profile', + templateUrl: './group-profile.component.html', + styleUrls: [ + 'group-profile.component.scss', + '../libs/css/material-icons.css', + '../libs/css/bootstrap.min.css', + ], +}) +export class GroupProfileComponent { + programs: ProgramMetadata[] = []; + + session: Session = null; + @Input() profile: GroupProfileInfo; + bridgeInfo: { [key:string]: { icon: string, name: string }} = {}; + collaborators: Collaborator[] = null; + groupInfo: GroupInfo; + + readonly _iconDataToUrl: (icon: IconReference, bridge_id: string) => string; + + 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._iconDataToUrl = iconDataToUrl.bind(this, environmentService); + } + + ngOnInit(): void { + } + + async openProgram(program: ProgramMetadata): Promise { + let programType = 'scratch'; + + if (program.type === 'flow_program') { + programType = 'flow'; + } + + this.router.navigateByUrl(`/programs/${program.id}/${programType}`); + } + + // 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/profiles/profile.service.ts b/frontend/src/app/profiles/profile.service.ts new file mode 100644 index 00000000..1e90c833 --- /dev/null +++ b/frontend/src/app/profiles/profile.service.ts @@ -0,0 +1,63 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { BridgeIndexData } from 'app/bridges/bridge'; +import { GroupInfo } from 'app/group'; +import { EnvironmentService } from '../environment.service'; +import { ProgramMetadata } from '../program'; +import { SessionService } from '../session.service'; +import { getUserPictureUrl, ground, getGroupPictureUrl } from 'app/utils'; + + +export type UserProfileInfo = { + name: string, + pictureUrl: string, + programs: ProgramMetadata[], + groups: GroupInfo[], + bridges: BridgeIndexData[], + id: string, +}; + +export type GroupProfileInfo = { + name: string, + pictureUrl: string, + programs: ProgramMetadata[], + collaborators: { name: string, id: string, picture?: string }[], + bridges: BridgeIndexData[], + id: string, +}; + +@Injectable() +export class ProfileService { + constructor( + private http: HttpClient, + private sessionService: SessionService, + private environmentService: EnvironmentService, + ) { + this.http = http; + this.sessionService = sessionService; + } + + public async getProfileFromUsername(username: string): Promise { + const url = `${this.environmentService.getApiRoot()}/users/by-name/${username}/profile`; + + const data : any = await this.http.get(url, { }).toPromise(); + + data.pictureUrl = getUserPictureUrl(this.environmentService, data.id); + data.groups = data.groups.map((group: GroupInfo) => ground(this.environmentService, group, 'picture')); + + return data as UserProfileInfo; + } + + public async getProfileFromGroupname(groupname: string): Promise { + const url = `${this.environmentService.getApiRoot()}/groups/by-name/${groupname}/profile`; + + const data : any = await this.http.get(url, {headers: this.sessionService.getAuthHeader()}).toPromise(); + + data.pictureUrl = getGroupPictureUrl(this.environmentService, data.id); + data.collaborators.forEach((collaborator: { name: string, picture: string; id: string; }) => { + collaborator.picture = getUserPictureUrl(this.environmentService, collaborator.id) + }); + + return data as GroupProfileInfo; + } +} diff --git a/frontend/src/app/profiles/user-profile.component.html b/frontend/src/app/profiles/user-profile.component.html new file mode 100644 index 00000000..a6a14a40 --- /dev/null +++ b/frontend/src/app/profiles/user-profile.component.html @@ -0,0 +1,70 @@ +
+
+
+
+ +
{{ profile.name }}
+
+
+
Groups
+ +
+
+ +
+
+ No public programs. +
+
+
+
Public Programs
+
+ +
+ + +
+
+ + +
+
{{program.name}}
+
+ + + + {{ bridgeInfo[bridgeId].name }} + + +
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/profiles/user-profile.component.scss b/frontend/src/app/profiles/user-profile.component.scss new file mode 100644 index 00000000..fec9b9dc --- /dev/null +++ b/frontend/src/app/profiles/user-profile.component.scss @@ -0,0 +1,522 @@ +.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; +} + +.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; + max-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.row { + margin-left: 0; + margin-right: 0; + + .user { + padding: 0; + } +} +.profile section { + width: 100%; + + &.groups { + width: max-content; + max-width: 100%; + min-width: 15ex; + + + .section-title { + padding: 0.25rem; + } + + .section-objects { + text-align: center; + } + } + + & > hr { + width: 12rem; + max-width: 100%; + background-color: #888; + } + + &.programs { + padding: 1rem 0.25rem 2rem 0.25rem; + + .program-list, .empty-section-explanation { + background-color: #eee; + border-radius: 4px; + } + + .empty-section-explanation { + padding: 1rem; + font-weight: bold; + } + } +} + +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 .bridge-connections-subsection { + border-top: 1px solid #aaa; +} + +section.bridges .bridge-connections-subsection 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; +} diff --git a/frontend/src/app/profiles/user-profile.component.ts b/frontend/src/app/profiles/user-profile.component.ts new file mode 100644 index 00000000..c45fa5e2 --- /dev/null +++ b/frontend/src/app/profiles/user-profile.component.ts @@ -0,0 +1,107 @@ +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 { IconReference } from 'app/connection'; +import { ConnectionService } from 'app/connection.service'; +import { EnvironmentService } from 'app/environment.service'; +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 { ProgramMetadata } from '../program'; +import { ProgramService } from '../program.service'; +import { Session } from '../session'; +import { SessionService } from '../session.service'; +import { getGroupPictureUrl, getUserPictureUrl, iconDataToUrl } from '../utils'; +import { ProfileService, UserProfileInfo } from './profile.service'; + + +@Component({ + // moduleId: module.id, + selector: 'app-my-user-profile', + templateUrl: './user-profile.component.html', + styleUrls: [ + 'user-profile.component.scss', + '../libs/css/material-icons.css', + '../libs/css/bootstrap.min.css', + ], +}) +export class UserProfileComponent { + programs: ProgramMetadata[] = []; + + session: Session = null; + profile: UserProfileInfo; + bridgeInfo: { [key:string]: { icon: string, name: string }} = {}; + collaborators: Collaborator[] = null; + groupInfo: GroupInfo; + + readonly _iconDataToUrl: (icon: IconReference, bridge_id: string) => string; + + 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._iconDataToUrl = iconDataToUrl.bind(this, environmentService); + + this.route.data + .subscribe((data: { user_profile: UserProfileInfo }) => { + this.profile = data.user_profile; + + this.profile.programs.sort((a, b) => { + return a.name.localeCompare(b.name, undefined, { ignorePunctuation: true, sensitivity: 'base' }); + }); + + this.profile.groups.sort((a, b) => { + return a.name.localeCompare(b.name, undefined, { ignorePunctuation: true, sensitivity: 'base' }); + }); + + this.profile.bridges.sort((a, b) => { + return a.name.localeCompare(b.name, undefined, { ignorePunctuation: true, sensitivity: 'base' }); + }); + + for (const bridge of this.profile.bridges) { + this.bridgeInfo[bridge.id] = { + name: bridge.name, + icon: iconDataToUrl(this.environmentService, bridge.icon, bridge.id) + }; + } + + }); + } + + ngOnInit(): void { + } + + async openProgram(program: ProgramMetadata): Promise { + let programType = 'scratch'; + + if (program.type === 'flow_program') { + programType = 'flow'; + } + + this.router.navigateByUrl(`/programs/${program.id}/${programType}`); + } + + // 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/program-detail.component.css b/frontend/src/app/program-detail.component.css deleted file mode 100644 index 1dba124a..00000000 --- a/frontend/src/app/program-detail.component.css +++ /dev/null @@ -1,51 +0,0 @@ -.program-pad #workspace { - width: 100%; -} - -.app-content { - overflow: hidden; -} - -#program-header { - border-bottom: 1px solid #AAA; - overflow-y: auto; - height: 3em; -} - -button#program-rename-button, -button#program-start-button, -button#program-delete-button, -button#program-stop-button, -button#advancedProgramControls { - vertical-align: top; - padding: 1ex; -} - -button#program-delete-button{ - float: right; -} - -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: 2em; -} - -.program-name > a { - padding-left: 1ex; -} - -.program-name .back-arrow { - vertical-align: middle; -} diff --git a/frontend/src/app/program-detail.component.html b/frontend/src/app/program-detail.component.html index 57beccba..1a481c15 100644 --- a/frontend/src/app/program-detail.component.html +++ b/frontend/src/app/program-detail.component.html @@ -1,48 +1,170 @@
-
+

arrow_back_ios - {{program.name}} - Loading program... + {{programName}} + Loading...

+ + + + Scroll to find more buttons + + + + + + + - + - + - - + + + + + + + + - +
-
+ + + + + + +
+
+
+ Loading… +
+
+ Connection lost. + + + to reconnect. +
+
+
+
+
+
+
diff --git a/frontend/src/app/program-detail.component.scss b/frontend/src/app/program-detail.component.scss new file mode 100644 index 00000000..a026b6a4 --- /dev/null +++ b/frontend/src/app/program-detail.component.scss @@ -0,0 +1,155 @@ +.program-pad #workspace { + width: 100%; +} + +.program-pad #workspace-read-only-marker { + width: 100%; + height: 100%; + position: absolute; + z-index: 999; + background-color: rgba(0,0,0,0.7); +} + +.program-pad #workspace-read-only-marker .message { + position: absolute; + top: 50%; + transform: translateY(-50%); + font-size: 250%; + color: white; + width: 100%; + text-align: center; +} + +.program-pad #workspace-read-only-marker .message button .action-icon { + font-size: inherit; + width: 1em; + vertical-align: sub; +} + +.app-content { + overflow: hidden; +} + +#program-header { + border-bottom: 1px solid #AAA; + overflow-y: auto; + height: 3em; +} + +#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#advanced-program-controls-button { + 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; +} + +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: 1ex; +} + +.program-name > a { + padding-left: 1ex; +} + +.program-name > .program-title { + display: inline-block; + width: 15ex; + max-width: 50vw; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.program-name .back-arrow { + vertical-align: middle; +} + +.viewer { + margin: 0; +} diff --git a/frontend/src/app/program-detail.component.ts b/frontend/src/app/program-detail.component.ts index 2c28ba9a..304a63fc 100644 --- a/frontend/src/app/program-detail.component.ts +++ b/frontend/src/app/program-detail.component.ts @@ -1,60 +1,120 @@ - -import {switchMap} from 'rxjs/operators'; -import { Component, Input, OnInit } from '@angular/core'; +import { isPlatformServer, Location } from '@angular/common'; +import { AfterViewInit, Component, Inject, Input, NgZone, OnInit, PLATFORM_ID, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatMenu } from '@angular/material/menu'; +import { MatDrawer } from '@angular/material/sidenav'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { ProgramContent, ScratchProgram } from './program'; +import { ToastrService } from 'ngx-toastr'; +import { Unsubscribable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { environment } from '../environments/environment'; +import { AssetService } from './asset.service'; +import { BlocklyEvent, BlockSynchronizer } from './blocks/BlockSynchronizer'; +import { Toolbox, ToolboxRegistration } from './blocks/Toolbox'; +import { ToolboxController } from './blocks/ToolboxController'; +import { BrowserService } from './browser.service'; +import { ProgramEditorSidepanelComponent } from './components/program-editor-sidepanel/program-editor-sidepanel.component'; +import { ConnectionService } from './connection.service'; +import { CustomBlockService } from './custom_block.service'; +import { CustomSignalService } from './custom_signals/custom_signal.service'; +import { DeleteProgramDialogComponent } from './DeleteProgramDialogComponent'; +import { ChangeProgramVisilibityDialog } from './dialogs/change-program-visibility-dialog/change-program-visibility-dialog.component'; +import { CloneProgramDialogComponent, CloneProgramDialogComponentData } from './dialogs/clone-program-dialog/clone-program-dialog.component'; +import { EnvironmentService } from './environment.service'; +import { MonitorService } from './monitor.service'; +import { ProgramContent, ProgramEditorEventValue, ScratchProgram, VisibilityEnum } from './program'; +import { EditorController } from './program-editors/editor-controller'; import { ProgramService } from './program.service'; - -import { Toolbox } from './blocks/Toolbox'; -import * as progbar from './ui/progbar'; /// import ScratchProgramSerializer from './program_serialization/scratch-program-serializer'; -import { MonitorService } from './monitor.service'; -import { CustomBlockService } from './custom_block.service'; - -import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; +import { SetProgramTagsDialogComponent } from './program_tags/SetProgramTagsDialogComponent'; import { RenameProgramDialogComponent } from './RenameProgramDialogComponent'; -import { DeleteProgramDialogComponent } from './DeleteProgramDialogComponent'; +import { ServiceService } from './service.service'; +import { Session } from './session'; +import { SessionService } from './session.service'; import { StopThreadProgramDialogComponent } from './StopThreadProgramDialogComponent'; -import { SetProgramTagsDialogComponent } from './program_tags/SetProgramTagsDialogComponent'; -import { ToolboxController } from './blocks/ToolboxController'; +import { Synchronizer } from './syncronizer'; import { TemplateService } from './templates/template.service'; -import { ServiceService } from './service.service'; -import { CustomSignalService } from './custom_signals/custom_signal.service'; +import * as progbar from './ui/progbar'; + + +type NonReadyReason = 'loading' | 'disconnected'; + +const SvgNS = "http://www.w3.org/2000/svg"; +const PRINT_MARGIN = 20; @Component({ selector: 'app-my-program-detail', templateUrl: './program-detail.component.html', - providers: [CustomBlockService, CustomSignalService, MonitorService, ProgramService, TemplateService, ServiceService], + providers: [ + AssetService, ConnectionService, CustomBlockService, CustomSignalService, + MonitorService, ProgramService, ServiceService, SessionService, + TemplateService + ], styleUrls: [ - 'program-detail.component.css', + 'program-detail.component.scss', 'libs/css/material-icons.css', 'libs/css/bootstrap.min.css', ], }) -export class ProgramDetailComponent implements OnInit { +export class ProgramDetailComponent implements OnInit, AfterViewInit { @Input() program: ProgramContent; - currentFillingInput: string; - workspace: Blockly.Workspace; - programUserName: string; + workspace: Blockly.WorkspaceSvg; programId: string; + programName: string; + environment: { [key: string]: any }; + session: Session; + + @ViewChild('drawer') drawer: MatDrawer; + @ViewChild('sidepanel') sidepanel: ProgramEditorSidepanelComponent; + toolboxController: ToolboxController; portraitMode: boolean; smallScreen: boolean; + patchedFunctions: {recordDeleteAreas: (() => void)} = { recordDeleteAreas: null }; + eventStream: Synchronizer; + isReady: boolean; + connectionLost: boolean; + private workspaceElement: HTMLElement; + + private cursorDiv: HTMLElement; + private cursorInfo: {[key: string]: HTMLElement}; + nonReadyReason: NonReadyReason; + + read_only: boolean = true; + can_admin: boolean = false; + + // HACK: Prevent the MatMenu import for being removed + private _pinRequiredMatMenuLibrary: MatMenu; + eventSubscription: Unsubscribable | null; + mutationObserver: MutationObserver | null; + blockSynchronizer: BlockSynchronizer; + visibility: VisibilityEnum; + constructor( + private browser: BrowserService, private monitorService: MonitorService, private programService: ProgramService, private customBlockService: CustomBlockService, private customSignalService: CustomSignalService, + private assetService: AssetService, private route: ActivatedRoute, private router: Router, - public dialog: MatDialog, + private _location: Location, private templateService: TemplateService, private serviceService: ServiceService, private notification: MatSnackBar, + private dialog: MatDialog, + private connectionService: ConnectionService, + private sessionService: SessionService, + private ngZone: NgZone, + private environmentService: EnvironmentService, + private toastr: ToastrService, + + @Inject(PLATFORM_ID) private platformId: Object ) { this.monitorService = monitorService; this.programService = programService; @@ -62,46 +122,112 @@ export class ProgramDetailComponent implements OnInit { this.customSignalService = customSignalService; this.route = route; this.router = router; - this.serviceService = serviceService; + this.serviceService = serviceService + this.isReady = false; + this.nonReadyReason = 'loading'; + + this.cursorInfo = {}; } ngOnInit(): void { - if (window && (window.innerWidth < window.innerHeight)) { + 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 = window.innerWidth < 750; + this.smallScreen = this.browser.window.innerWidth < 750; + + progbar.track(new Promise(async (resolve, reject) => { + + this.session = await this.sessionService.getSession(); - progbar.track(new Promise((resolve) => { this.route.params.pipe( switchMap((params: Params) => { - this.programUserName = params['user_id']; - this.programId = params['program_id']; - return this.programService.getProgram(params['user_id'], params['program_id']).catch(err => { - console.error("Error:", err); - this.goBack(); - throw Error("Error loading"); - }); + const user = params['user_id']; + if (user) { + const programName = params['program_id']; + + return this.programService.getProgram(user, programName).catch(err => { + if (!this.session.active) { + this.router.navigate(['/login'], {replaceUrl:true}); + reject(); + this.toastr.error(err.message, "Error loading"); + throw Error("Error loading"); + } + else { + console.error("Error:", err); + this.toastr.error(err.message, "Error loading"); + return null; + } + }); + } + else { + this.programId = params['program_id']; + return this.programService.getProgramById(this.programId).catch(err => { + if (!this.session.active) { + this.router.navigate(['/login'], {replaceUrl:true}); + reject(); + throw Error("Error loading"); + } + else { + console.error("Error:", err); + this.toastr.error(err.message, "Error loading"); + return null; + } + }) + } })) .subscribe(program => { - this.prepareWorkspace().then((controller: ToolboxController) => { + if (program === null) { + return; + } + + this.programName = program.name; + this.programId = program.id; + this.read_only = program.readonly; + this.visibility = program.visibility; + this.can_admin = program.can_admin; + + this.prepareWorkspace(program).then((controller: ToolboxController) => { this.program = program; this.load_program(controller, program); resolve(); }).catch(err => { console.error("Error:", err); - this.goBack(); + resolve(); + this.toastr.error(JSON.stringify(err), "Error loading"); }); }); })); - this.currentFillingInput = ''; + } + + 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 }); } /** * Check if an DOM element is a Scratch block object. */ - is_block(blockCandidate: Element) { + is_block(blockCandidate: Element): boolean { if (blockCandidate.tagName === undefined) { return false; } @@ -125,17 +251,12 @@ export class ProgramDetailComponent implements OnInit { // Clean the contents of the block const next = child.getElementsByTagName('next')[0]; - let next_blocks = []; + let next_blocks: ChildNode[] = []; if (next !== undefined) { this.removeNonExistingBlocks(next, controller); next_blocks = (Array.from(next.childNodes) .filter((x: Element) => this.is_block(x))); - - if (next_blocks.length == 0) { - child.removeChild(next); - continue; - } } const _type = child.getAttribute('type'); @@ -143,50 +264,356 @@ export class ProgramDetailComponent implements OnInit { if (!controller.isKnownBlock(_type)) { // If it's not known, pull the next into the top level "function" if (next !== undefined) { - next.removeChild(next_blocks[0]); - dom.insertBefore(next_blocks[0], child); + if (next_blocks.length > 0) { + next.removeChild(next_blocks[0]); + dom.insertBefore(next_blocks[0], child); + } child.removeChild(next); this.removeNonExistingBlocks(next, controller); } // Remove top level dom.removeChild(child); - console.debug("To replace:", child, 'with', next); } } } load_program(controller: ToolboxController, program: ProgramContent) { - const xml = Blockly.Xml.textToDom(program.orig); + let source = program.orig; + if (program.checkpoint) { + source = program.checkpoint; + } + const xml = Blockly.Xml.textToDom(source); this.removeNonExistingBlocks(xml, controller); (Blockly.Xml as any).clearWorkspaceAndLoadFromXml(xml, this.workspace); } + becomeReady() { + this.isReady = true; + } + + initializeListeners() { + this.initializeEventSynchronization(); + } + + initializeEventSynchronization() { + // Initialize editor event listeners + // This is used for collaborative editing. + + if (this.read_only) { + this.becomeReady(); // We won't have to wait for the last state to get loaded + return; + } + + this.eventStream = this.programService.getEventStream(this.program.id); + this.blockSynchronizer = new BlockSynchronizer(this.eventStream, this.checkpointProgram.bind(this)); + + const onCreation: {[key: string]: boolean} = {}; + const mirrorEvent = (event: BlocklyEvent) => { + if (event instanceof Blockly.Events.Ui) { + return; // Don't mirror UI events. + } + + if (this.blockSynchronizer.isDuplicated(event)) { + return; // Avoid mirroring events received from the net + } + + // Convert event to JSON. This could then be transmitted across the net. + const json: any = event.toJson() as any; + + // Avoid passing messages about being created outside of this editor + if (onCreation[json.blockId]) { + console.debug('Skipping sending message to block on creation'); + return; + } + + try { + if (this.isReady) { + this.eventStream.push({ type: 'blockly_event', value: json, save: true }); + } + } + catch (error) { + console.log(error); + } + } + + this.eventSubscription = this.eventStream.subscribe( + { + next: (ev: ProgramEditorEventValue) => { + if (ev.type === 'blockly_event') { + const event = Blockly.Events.fromJson(ev.value, this.workspace); + if ((event as any).type === 'comment_create' + || (event as any).type === 'comment_delete') { + + console.debug("Ignoring changes in comments") + return; + } + + this.blockSynchronizer.receivedEvent(event as BlocklyEvent); + if (ev.value.type === 'create') { + onCreation[ev.value.blockId] = true; + } + else if (ev.value.type === 'endDrag') { + delete onCreation[ev.value.blockId]; + } + + try { + event.run(true); + } + catch(err) { + this.toastr.error("Error loading updates"); + console.log("EV", ev, event); + console.error(err); + } + } + else 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') { + this.becomeReady(); + } + }, + error: (error: any) => { + console.error("Error obtainig editor events:", error); + }, + complete: () => { + console.log("Disconnected"); + this.nonReadyReason = 'disconnected'; + this.isReady = false; + } + } + ); + + const onMouseEvent = ((ev: MouseEvent) => { + if (ev.buttons) { + return; + } + + const disp = ProgramDetailComponent.getEditorPosition(this.workspaceElement); + + const rect = this.workspaceElement.getBoundingClientRect(); + const cursorInWorkspace = { x: ev.x - rect.left, y: ev.y - rect.top } + + const posInCanvas = { + x: (cursorInWorkspace.x - disp.x) / disp.scale, + y: (cursorInWorkspace.y - disp.y) / disp.scale, + } + + this.eventStream.push({ type: 'cursor_event', value: posInCanvas }) + }); + + this.workspace.addChangeListener(mirrorEvent); + this.workspaceElement.onmousemove = onMouseEvent; + this.workspaceElement.onmouseup = onMouseEvent; + } + + /* 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.workspaceElement.getBoundingClientRect(); + const disp = ProgramDetailComponent.getEditorPosition(this.workspaceElement); + const cursor = this.getPointer(pos.id); + + const posInScreen = { + x: pos.x * disp.scale + disp.x + rect.left, + y: pos.y * disp.scale + disp.y + 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'; + } + } + + checkpointProgram() { + const xml = Blockly.Xml.workspaceToDom(this.workspace); + + // Remove comments + for (const comment of Array.from(xml.getElementsByTagName('COMMENT'))) { + comment.parentNode.removeChild(comment); + } + + const content = Blockly.Xml.domToPrettyText(xml); + + return this.programService.checkpointProgram(this.program.id, content); + } + + static getEditorPosition(workspaceElement: HTMLElement): {x:number, y: number, scale: number} | null { + + const SVG_TRANSFORM_TRANSLATE = 2; + const SVG_TRANSFORM_SCALE = 3; + + let reference: HTMLElement | null = workspaceElement.getElementsByClassName('blocklySvg')[0].getElementsByClassName('blocklyBlockCanvas')[0] as HTMLElement; + + // Not dragging, so we can directly use Blockly's canvas as reference + if (reference) { + const transformations : SVGTransformList = (reference as any).transform.baseVal; + let x = 0, y = 0, scale = 1; + + for (let i = 0; i < transformations.numberOfItems; i++) { + const t = transformations.getItem(i); + + if (t.type === SVG_TRANSFORM_TRANSLATE) { + x = t.matrix.e; + y = t.matrix.f; + } + else if (t.type === SVG_TRANSFORM_SCALE) { + scale = t.matrix.a; + } + } + + return { x, y, scale}; + } + + // If we have to use the drag surface as refernce this is a bit less clean + else { + reference = workspaceElement.getElementsByClassName('blocklyWsDragSurface')[0] as HTMLElement; + + if (!reference) { + console.error("Could not find reference"); + return null; + } + + // Take transformation from the drag surface + const result = /translate3d\((-?\d+)px, *(-?\d+)px, *-?\d+px\)/.exec(reference.style.transform); + + const x = parseInt(result[1]), y = parseInt(result[2]); + + // Take scale from it's inner + const canvas = reference.getElementsByClassName('blocklyBlockCanvas')[0] as HTMLElement; + + let scale = 1; + const transformations : SVGTransformList = (canvas as any).transform.baseVal; + + for (let i = 0; i < transformations.numberOfItems; i++) { + const t = transformations.getItem(i); + + if (t.type === SVG_TRANSFORM_SCALE) { + scale = t.matrix.a; + } + } + + return { x, y, scale }; + } + + } + patch_flyover_area_deletion() { - const orig = (Blockly.WorkspaceSvg.prototype as any).recordDeleteAreas_; + if ((Blockly.WorkspaceSvg.prototype as any).recordDeleteAreas_.orig) { + return; + } + + this.patchedFunctions.recordDeleteAreas = (Blockly.WorkspaceSvg.prototype as any).recordDeleteAreas_; (Blockly.WorkspaceSvg.prototype as any).recordDeleteAreas_ = () => { - orig.bind(this.workspace)(); + this.patchedFunctions.recordDeleteAreas.bind(this.workspace)(); // Disable toolbox delete area use trashcan for deletion const tbDelArea = (this.workspace as any).deleteAreaToolbox_; - tbDelArea.left = -100; - tbDelArea.top = -100; - tbDelArea.width = 0; - tbDelArea.height = 0; + if (tbDelArea) { + tbDelArea.left = -100; + tbDelArea.top = -100; + tbDelArea.width = 0; + tbDelArea.height = 0; + } } + (Blockly.WorkspaceSvg.prototype as any).recordDeleteAreas_.orig = this.patchedFunctions.recordDeleteAreas; } - prepareWorkspace(): Promise { + async reloadToolbox(): Promise { + const toolbox = new Toolbox( + this.program, + this.assetService, + this.monitorService, + this.customBlockService, + this.dialog, + this.templateService, + this.serviceService, + this.customSignalService, + this.connectionService, + this.sessionService, + this.environmentService, + this.read_only, + this.toolboxController, + ); + + const [toolboxXml, registrations, _controller] = await toolbox.inject(); + + this.toolboxController.setToolbox(toolboxXml); + this.toolboxController.update(); + this.performToolboxRegistrations(registrations); + } + + prepareWorkspace(program: ProgramContent): Promise { // For consistency and because it affects the positioning of the bottom drawer. this.reset_header_scroll(); return new Toolbox( + program, + this.assetService, this.monitorService, this.customBlockService, this.dialog, this.templateService, this.serviceService, this.customSignalService, + this.connectionService, + this.sessionService, + this.environmentService, + this.read_only, ) .inject() .then(([toolbox, registrations, controller]) => { @@ -196,17 +623,24 @@ export class ProgramDetailComponent implements OnInit { }); } - injectWorkspace(toolbox: HTMLElement, registrations: Function[], controller: ToolboxController) { + injectWorkspace(toolbox: HTMLElement, registrations: ToolboxRegistration[], controller: ToolboxController) { // Avoid initializing it twice - if (this.workspace !== undefined) { + if (this.workspace) { return; } + this.cursorDiv = document.getElementById('program-cursors'); + + this.workspaceElement = document.getElementById('workspace'); + const programHeaderElement = document.getElementById('program-header'); - const workspaceElement = document.getElementById('workspace'); - this.hide_workspace(workspaceElement); - window.onresize = () => this.calculate_size(workspaceElement); - this.calculate_size(workspaceElement); + this.hide_workspace(this.workspaceElement); + this.browser.window.onresize = (() => { + this.calculate_size(this.workspaceElement); + this.calculate_program_header_size(programHeaderElement); + }); + this.calculate_size(this.workspaceElement); + this.calculate_program_header_size(programHeaderElement); const rtl = false; const soundsEnabled = false; let toolbarLayout = { horizontal: false, position: 'start' }; @@ -219,7 +653,7 @@ export class ProgramDetailComponent implements OnInit { disable: false, collapse: true, media: '../assets/scratch-media/', - readOnly: false, + readOnly: this.read_only, trashcan: true, rtl: rtl, scrollbars: true, @@ -239,15 +673,15 @@ export class ProgramDetailComponent implements OnInit { fieldShadow: 'rgba(255, 255, 255, 0.3)', dragShadowOpacity: 0.6 } - }); - - for (const reg of registrations) { - reg(this.workspace); - } + }) as Blockly.WorkspaceSvg; + this.performToolboxRegistrations(registrations); this.toolboxController = controller; controller.setWorkspace(this.workspace); - controller.update(); + + if (!this.read_only) { + controller.update(); // Setting the toolbox when it can't be shown would generate an error + } // HACK: // Defer a hide action, this is to compsensate for (what feels like) @@ -260,7 +694,11 @@ export class ProgramDetailComponent implements OnInit { // sidebar would be. To compensate for this we set the visibility // of the workspace to 'hidden' until the process has finished. setTimeout(() => { - this.show_workspace(workspaceElement); + this.show_workspace(this.workspaceElement); + + // Listeners have to be started after the whole initialization is + // done to avoid capturing the events happening during the start-up. + this.initializeListeners(); if (this.portraitMode || this.smallScreen){ this.hide_block_menu(); @@ -271,12 +709,23 @@ export class ProgramDetailComponent implements OnInit { const dragContainer = document.querySelector('.blocklyBlockDragSurface>g'); dragContainer.setAttribute('filter', 'drop-shadow(0 0 5px rgba(0,0,0,0.5))'); + + if (this.portraitMode || this.smallScreen) { + this.patch_flyover_area_deletion(); + } }, 0); this.patch_blockly(); + } - if (this.portraitMode || this.smallScreen) { - this.patch_flyover_area_deletion(); + performToolboxRegistrations(registrations: ToolboxRegistration[]) { + const editorController: EditorController = { + reloadToolbox: () => { + this.reloadToolbox(); + } + }; + for (const reg of registrations) { + reg(this.workspace, editorController, this.ngZone); } } @@ -314,9 +763,19 @@ export class ProgramDetailComponent implements OnInit { const header_pos = this.get_position(header); const header_end = header_pos.y + header.clientHeight; - const window_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + const window_height = Math.max(document.documentElement.clientHeight, this.browser.window.innerHeight || 0); - workspace.style.height = (window_height - header_end) + 'px'; + 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'); + } } reset_zoom() { @@ -371,7 +830,7 @@ export class ProgramDetailComponent implements OnInit { (category as any).ontouchend = move_and_show; } - this.workspace.addChangeListener((event) => { + this.workspace.addChangeListener((event: Blockly.Events.Change__Class | Blockly.Events.Create__Class | Blockly.Events.Delete__Class | Blockly.Events.Move__Class) => { if (event.type === Blockly.Events.BLOCK_CREATE) { component.hide_block_menu(); } @@ -379,11 +838,15 @@ export class ProgramDetailComponent implements OnInit { } hide_block_menu() { - (this.workspace as any).getFlyout().setVisible(false); + if ((this.workspace as any).getFlyout()) { + (this.workspace as any).getFlyout().setVisible(false); + } } show_block_menu() { - (this.workspace as any).getFlyout().setVisible(true); + if ((this.workspace as any).getFlyout()) { + (this.workspace as any).getFlyout().setVisible(true); + } } show_workspace(workspace: HTMLElement) { @@ -391,7 +854,7 @@ export class ProgramDetailComponent implements OnInit { // Elements might have moved around. // We trigger a resize to notify SVG elements. - window.dispatchEvent(new Event('resize')); + this.browser.window.dispatchEvent(new Event('resize')); } hide_workspace(workspace: HTMLElement) { @@ -399,19 +862,154 @@ export class ProgramDetailComponent implements OnInit { } goBack(): boolean { - this.router.navigate(['/dashboard']) + this.dispose(); + this._location.back(); return false; } - sendProgram() { + force_reload() { + location = location; + } + + dispose() { + try { + if (this.eventSubscription) { + this.eventSubscription.unsubscribe(); + this.eventSubscription = null; + } + + if (this.sidepanel) { + this.sidepanel.dispose(); + } + + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } + + if (this.blockSynchronizer) { + this.blockSynchronizer.close(); + this.blockSynchronizer = null; + } + + this.eventStream = null; + } catch(error) { + console.error("Error closing event stream:", error); + } + + try { + this.workspace.dispose(); + this.workspace = null; + } catch(error) { + console.error("Error disposing workspace:", error); + } + + // Restore the patched function, to cleaup the state. + try { + if (this.patchedFunctions.recordDeleteAreas) { + (Blockly.WorkspaceSvg.prototype as any).recordDeleteAreas_ = this.patchedFunctions.recordDeleteAreas; + this.patchedFunctions.recordDeleteAreas = null; + } + } catch (error) { + console.error("Error restoring recordDeleteAreas:", error); + } + } + + async sendProgram(): Promise { + // Get workspace + const xml = Blockly.Xml.workspaceToDom(this.workspace); + + // Remove comments + for (const comment of Array.from(xml.getElementsByTagName('COMMENT'))) { + comment.parentNode.removeChild(comment); + } + + // Serialize result + let program; + try { + const serializer = new ScratchProgramSerializer(this.toolboxController); + const serialized = serializer.ToJson(xml); + program = new ScratchProgram(this.program, + serialized.parsed, + serialized.orig); + } + catch (error) { + this.toastr.error(error, 'Invalid program', { + closeButton: true, + progressBar: true, + }); + + console.error(error); + return; + } + + // Send update + const button = document.getElementById('program-start-button'); + if (button){ + button.classList.add('started'); + button.classList.remove('completed'); + } + + const result = await this.programService.updateProgram(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)), + }; + + // Get workspace const xml = Blockly.Xml.workspaceToDom(this.workspace); + // Remove comments + for (const comment of Array.from(xml.getElementsByTagName('COMMENT'))) { + comment.parentNode.removeChild(comment); + } + + // Serialize result const serializer = new ScratchProgramSerializer(this.toolboxController); const serialized = serializer.ToJson(xml); - const program = new ScratchProgram(this.program, - serialized.parsed, - serialized.orig); - this.programService.updateProgram(this.programUserName, program); + + programData.program.orig = serialized.orig; + 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}/scratch`], { replaceUrl: false }); + }); } renameProgram() { @@ -421,13 +1019,14 @@ export class ProgramDetailComponent implements OnInit { data: programData }); - dialogRef.afterClosed().subscribe(result => { + dialogRef.afterClosed().subscribe(async (result) => { if (!result) { console.log("Cancelled"); return; } - const rename = (this.programService.renameProgram(this.programUserName, this.program, programData.name) + await this.sendProgram(); + const rename = (this.programService.renameProgram(this.program, programData.name) .catch(() => { return false; }) .then(success => { if (!success) { @@ -435,21 +1034,42 @@ export class ProgramDetailComponent implements OnInit { } this.program.name = programData.name; - const path = document.location.pathname.split("/"); - path[path.length - 1] = encodeURIComponent(this.program.name); - - this.router.navigate([path.join("/")]); - console.log("Changing name to", this.program); })); 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: [], // Initially empty, to be updated by dialog + tags: [] as string[], // Initially empty, to be updated by dialog }; const dialogRef = this.dialog.open(SetProgramTagsDialogComponent, { @@ -462,7 +1082,7 @@ export class ProgramDetailComponent implements OnInit { return; } - const update = (this.programService.updateProgramTags(this.program.owner, this.program.id, data.tags) + const update = (this.programService.updateProgramTags(this.program.id, data.tags) .then((success) => { if (!success) { return; @@ -495,7 +1115,7 @@ export class ProgramDetailComponent implements OnInit { return; } - const stopThreads = (this.programService.stopThreadsProgram(this.program.owner, this.program.id) + const stopThreads = (this.programService.stopThreadsProgram(this.program.id) .catch(() => { return false; }) .then(success => { if (!success) { @@ -509,8 +1129,160 @@ export class ProgramDetailComponent implements OnInit { }); } + async downloadScreenshot() { + // See: https://stackoverflow.com/q/23218174 + const canvas = this.workspaceElement.getElementsByTagNameNS(SvgNS, 'svg')[0].cloneNode(true) as SVGElement; + const name = this.program.name.replace(/[^a-zA-Z0-9]/g, '-').replace(/--+/g, '-') + '.svg'; + + // Pull style file + const styles = document.createElementNS(SvgNS, 'style'); + const styleSheet = Array.from((Blockly.Css.styleSheet_ as any).cssRules).map((r: any) => r.cssText).join('\n'); + + styles.innerHTML = ('/* */<'); + + 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) { + continue; + } + + let stripped = false; + while (image.href.baseVal.startsWith('../')) { + image.href.baseVal = image.href.baseVal.substring(3); + stripped = true; + } + + if (stripped) { + image.href.baseVal = '/' + image.href.baseVal; + } + + if (image.href.baseVal.startsWith('/')) { + // Image relative to current domain + image.href.baseVal = baseServerPath + image.href.baseVal; + } + } + + // Remove controls + for (const cls of ['blocklyMainBackground', 'blocklyTrash', 'blocklyBubbleCanvas', 'blocklyScrollbarBackground', 'blocklyZoom']) { + for (const e of Array.from(canvas.getElementsByClassName(cls))) { + e.parentNode.removeChild(e); + } + } + + // Adjust viewport + this.adjustCanvasViewport(canvas); + + // 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(/^ { + + const reposition = { x: 0, y: 0}; + let elem = block.svgGroup_; + + const SVG_TRANSFORM_TRANSLATE = 2; + + while (elem !== topLevel) { + if (elem.transform) { + for (let i = 0; i < elem.transform.baseVal.numberOfItems; i++) { + const t = elem.transform.baseVal.getItem(i); + if (t.type === SVG_TRANSFORM_TRANSLATE) { + reposition.x += t.matrix.e; + reposition.y += t.matrix.f; + } + } + } + elem = elem.parentElement as any as SVGGElement; + } + + return { + left: reposition.x, + top: reposition.y, + right: reposition.x + block.width, + bottom: reposition.y + block.height, + } + } + + const rect = getRect(blocks[0]); + + for (const block of blocks) { + const blockArea = getRect(block); + + + if (blockArea.left < rect.left) { + rect.left = blockArea.left; + } + if (blockArea.top < rect.top) { + rect.top = blockArea.top; + } + if (blockArea.right > rect.right) { + rect.right = blockArea.right; + } + if (blockArea.bottom > rect.bottom) { + rect.bottom = blockArea.bottom; + } + } + + const width = rect.right - rect.left; + const height = rect.bottom - rect.top; + + canvas.getElementsByClassName('blocklyBlockCanvas')[0].removeAttribute('transform'); + + canvas.setAttribute('viewBox', + `${rect.left - PRINT_MARGIN} ${rect.top - PRINT_MARGIN} ${width + PRINT_MARGIN} ${height + PRINT_MARGIN}`); + canvas.removeAttribute('width'); + canvas.removeAttribute('height'); + } + deleteProgram() { - const programData = { name: this.program.name }; + const programData = { name: this.programName || '*error getting program name*' }; const dialogRef = this.dialog.open(DeleteProgramDialogComponent, { data: programData @@ -522,7 +1294,7 @@ export class ProgramDetailComponent implements OnInit { return; } - const deletion = (this.programService.deleteProgram(this.programUserName, this.program) + const deletion = (this.programService.deleteProgramById(this.programId) .catch(() => { return false; }) .then(success => { if (!success) { @@ -534,4 +1306,49 @@ export class ProgramDetailComponent implements OnInit { 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(); + } + + onToggleMark = (blockId: string, activate: boolean, message: string) => { + if (activate) { + this.workspace.getBlockById(blockId).setCommentText(message); + } + else { + this.workspace.getBlockById(blockId).setCommentText(null); + } + } } diff --git a/frontend/src/app/program-editors/editor-controller.ts b/frontend/src/app/program-editors/editor-controller.ts new file mode 100644 index 00000000..1bcd402a --- /dev/null +++ b/frontend/src/app/program-editors/editor-controller.ts @@ -0,0 +1,3 @@ +export interface EditorController { + reloadToolbox: () => void, +}; diff --git a/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-compiler.ts b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-compiler.ts new file mode 100644 index 00000000..a550d151 --- /dev/null +++ b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-compiler.ts @@ -0,0 +1,449 @@ +import { ResolvedCustomBlock } from '../../custom_block'; +import { get_block_from_base_toolbox } from '../../flow-editor/base_toolbox_description'; +import { InputPortDefinition, MessageType, OutputPortDefinition } from '../../flow-editor/flow_block'; +import { AtomicFlowBlockOperationType, AtomicFlowBlockOptions, BLOCK_TYPE as ATOMIC_BLOCK_TYPE, isAtomicFlowBlockData } from '../../flow-editor/atomic_flow_block'; +import { BLOCK_TYPE as VALUE_BLOCK_TYPE } from '../../flow-editor/direct_value'; +import { CompiledFlowGraph, FlowGraph, FlowGraphNode } from "../../flow-editor/flow_graph"; +import { compile } from "../../flow-editor/graph_analysis"; +import { uuidv4 } from '../../flow-editor/utils'; +import { ISpreadsheetToolbox } from "./spreadsheet-toolbox"; +import { reverse_index_connections, EdgeIndex } from '../../flow-editor/graph_utils'; +import { isUiFlowBlockData } from '../../flow-editor/ui-blocks/ui_flow_block'; +import { find_upstream } from '../../flow-editor/graph_transformations'; +import { get_block_inputs, get_block_outputs, get_block_message } from '../../flow-editor/toolbox_builder'; + +export function colName(index: number) { + const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + index -= 1; + if (letters[index]) { + return letters[index]; + } + else { + return letters[index / letters.length] + letters[index % letters.length]; + } +} + +type SpreadsheetConstant = { + type: 'constant', + value: any, +}; + +type SpreadsheetCall = { + type: 'call', + func_name: string, + arguments: SpreadsheetOperation[], +} + +type SpreadsheetCellRef = { + type: 'cell-ref', + value: string, +} + +type SpreadsheetOperation = SpreadsheetConstant | SpreadsheetCall | SpreadsheetCellRef; + +const INFIX_CHARACTERS = [ '+' ]; +const STRING_CHARS = [ "'", '"' ]; + +// See: https://regex101.com/r/1cZtZG/5 +const TOKENIZE_REGEX = new RegExp( + /^((?(?[\da-z_]+)\((?.*)\))|(?"([^"]|\")*")|(?(\s*))|(?\+)|(?[A-Z]{1,2}\d+)|(?-?\d+))$/ +); + +// See: https://regex101.com/r/3aGdXJ/1/ +const ARGUMENT_REGEX = new RegExp( + /^(?(?[\da-z_]+)(?\((.*)\)))|(?"([^"]|\")*")|(?\+)|(?[A-Z]{1,2}\d+)|(?\d+)(?.*)/ +); + + + +function parse_op(data: string): SpreadsheetOperation { + const tokens = parse_tokens(data); + if (tokens.length !== 1) { + throw Error(`Unexpected number of tokens (expected 1, found ${tokens.length}): ${data}`); + } + return tokens[0]; +} + +function parse_tokens(data: string): SpreadsheetOperation[] { + data = data.trim(); + const operations = [] as SpreadsheetOperation[]; + let tokens_in_arg = [] as SpreadsheetOperation[]; + + let in_string_char = null; + let escaped = false; + let current_token = [] as string[]; + let next_on = tokens_in_arg; + let next_on_swap_pos = 0; + let arg_start_pos = 0; + let m; + for (let idx = 0; idx < data.length; idx++){ + if (data[idx] === ' ') { + // Ignore + } + else if (data[idx] === ',') { + if (next_on !== tokens_in_arg) { + throw Error(`Uncomplete infix operation: ${data.substr(next_on_swap_pos)}`); + } + if (tokens_in_arg.length != 1) { + throw Error(`Unexpected number of tokens in arg (expected 1, found ${tokens_in_arg.length}): ${data.substr(arg_start_pos)}`); + } + operations.push(tokens_in_arg[0]); + tokens_in_arg = []; + next_on = tokens_in_arg; + + arg_start_pos = idx + 1; + } + else if (m = data.substr(idx).match(/^(?[a-zA-Z0-9_]+)\(/)) { + // Function call + const name = m.groups["name"]; + + const call_start_idx = idx; + const arg_start_idx = idx + name.length + 1; + idx = arg_start_idx - 1; + let parens_count = 0; + + // Find the end of the function call + for(; idx < data.length; idx++) { + if (in_string_char) { + if (escaped) { + escaped = false; + } + else if (data[idx] === '\\') { + escaped = true; + } + else if (data[idx] === in_string_char) { + in_string_char = null; + } + } + else if (STRING_CHARS.indexOf(data[idx]) >= 0) { + in_string_char = data[idx]; + } + else if (data[idx] === '(') { + parens_count++; + } + else if (data[idx] === ')') { + parens_count--; + if (parens_count === 0) { + break; + } + } + } + + if (idx === data.length){ + throw Error(`Uncomplete call: ${data.substr(call_start_idx)}`); + } + + const args = arg_start_idx < idx + ? parse_tokens(data.substring(arg_start_idx, idx)) + : []; + + next_on.push({ + type: 'call', + func_name: name, + arguments: args, + }); + next_on = tokens_in_arg; + } + else if (STRING_CHARS.indexOf(data[idx]) >= 0) { + // Parse string + in_string_char = data[idx]; + while (in_string_char) { + idx++; + if (escaped) { + current_token.push(escape_char(data[idx])); + escaped = false; + } + else if (data[idx] === '\\') { + escaped = true; + } + else if (data[idx] === in_string_char) { + next_on.push({ + type: 'constant', + value: current_token.join(''), + }); + next_on = tokens_in_arg; + current_token = []; + in_string_char = null; + } + else { + current_token.push(data[idx]); + } + } + } + else if (m = data.substr(idx).match(/^[A-Z]{1,2}\d+/)) { + let token = m[0]; + next_on.push({ + type: 'cell-ref', + value: token, + }); + next_on = tokens_in_arg; + idx += token.length - 1; + } + else if (m = data.substr(idx).match(/^\d+/)) { + let token = m[0]; + next_on.push({ + type: 'constant', + value: parseFloat(token), + }); + next_on = tokens_in_arg; + idx += token.length - 1; + } + else if (data[idx] === '+') { + // Note that this does not take into account + const left = tokens_in_arg.pop(); + const infix_op: SpreadsheetOperation = { + type: 'call', + func_name: get_infix_operation(data[idx]), + arguments: [ + left, + ] + }; + next_on.push(infix_op); + next_on = infix_op.arguments; + next_on_swap_pos = idx; + } + else { + throw Error(`Error parsing: [${data.substr(0, idx)}] → [${data.substr(idx)}]`); + } + } + + if (next_on !== tokens_in_arg) { + throw Error(`Uncomplete infix operation: ${data.substr(next_on_swap_pos)}`); + } + if (tokens_in_arg.length != 1) { + throw Error(`Unexpected number of tokens in arg (expected 1, found ${tokens_in_arg.length}): [${data.substr(0,arg_start_pos)}] → [${data.substr(arg_start_pos)}]`); + } + operations.push(tokens_in_arg[0]); + + return operations; +} + +function escape_char(char: string): string { + const escape_mapping: {[key: string]: string} = { + '"': '"', + "'": "'", + "n": '\n', + "r": '\r', + "\\": '\\', + }; + + return escape_mapping[char] +} + +function get_infix_operation(char: string): string { + const mapping: {[key: string]: string} = { + '+': 'operator_add', + }; + + return mapping[char] +} + +function parse_cell(data: string): SpreadsheetOperation { + data = data.trim(); + if (!data.startsWith('=')) { + return { + type: 'constant', + value: data + } + } + + return parse_op(data.substr(1)); +} + + +export function build_graph(orig: {[key: string]: string}, toolbox: ISpreadsheetToolbox): FlowGraph { + const g: FlowGraph = { nodes: {}, edges: [] }; + + let deferred_links: (() => void)[] = []; + for (const id of Object.keys(orig)) { + const data = orig[id]; + const op = parse_cell(data); + + deferred_links = deferred_links.concat(add_op_to_graph(op, toolbox, g, id)); + } + + for (const deferred of deferred_links) { + deferred(); + } + + for (const id of Object.keys(orig)) { + const rev_conn_index = reverse_index_connections(g); + wire_pulse(id, g, rev_conn_index); + } + + return g; +} + +function wire_pulse(id: string, g: FlowGraph, rev_conn_index: EdgeIndex) { + const target = g.nodes[id]; + if (isAtomicFlowBlockData(target.data)) { + if (target.data.value.options.type === 'getter') { + return; + } + } + else if (isUiFlowBlockData(target.data)) { + throw Error(`Unexpected UI block in spreadsheet`); + } + else { + return; + } + + const upstream = find_upstream(g, id, rev_conn_index, + (_node_id: string, node: FlowGraphNode) => { + if (isAtomicFlowBlockData(node.data)) { + if (node.data.value.options.type !== 'getter') { + return 'capture'; + } + else { + return 'continue'; + } + } + else if (isUiFlowBlockData(node.data)) { + throw Error(`Unexpected UI block in spreadsheet`); + } + + return 'stop'; // Ignore values and enums + }); + for (const node of upstream) { + g.edges.push({ + from: { + id: node, + output_index: 0, + }, + to: { + id: id, + input_index: 0, + } + }); + } +} + +function add_op_to_graph(op: SpreadsheetOperation, toolbox: ISpreadsheetToolbox, g: FlowGraph, id: string ): (() => void)[] { + if (op.type === 'constant') { + g.nodes[id] = { + data: { + type: VALUE_BLOCK_TYPE, + value: { + value: op.value, + } + } + } + return []; + } + else if (op.type === 'cell-ref') { + throw Error("CELL-REF not implemented as operation"); + } + + const call = toolbox.blockMap[op.func_name]; + let options: AtomicFlowBlockOptions; + + if (call) { + const block = call.block; + + const block_type = block.block_type as AtomicFlowBlockOperationType; + + const pulseOutputs = [] as OutputPortDefinition[]; + const pulseInputs = [] as InputPortDefinition[]; + + if (block_type === 'operation' || block_type === 'trigger') { + pulseOutputs.push({ + type: 'pulse', + }) + } + + if (block_type === 'operation') { + pulseInputs.push({ + type: 'pulse', + }) + } + + const [message, _translationTable] = get_block_message(block); + + options = { + type: block_type, + block_function: block.id, + message: message, + inputs: pulseInputs.concat(get_block_inputs(block)), + outputs: pulseOutputs.concat(get_block_outputs(block)), + } as AtomicFlowBlockOptions; + } + else { + options = get_block_from_base_toolbox(op.func_name); + } + + const block_type = options.type; + + g.nodes[id] = { + data: { + type: ATOMIC_BLOCK_TYPE, + value: { + options: options, + slots: {}, + synthetic_input_count: 0, + synthetic_output_count: 0, + } + } + } + + let idx = -1; + if (block_type === 'operation') { + idx++; // First input is pulse + } + + let deferred_links: (() => void)[] = []; + for (const arg of op.arguments) { + idx++; // First argument is index: 0 + + if (arg.type === 'cell-ref') { + // Cells are referenced directly, to avoid unnecessary intermediate steps + + // These links might need information not yet present on the graph, + // so their addition is deferred. Keep original reference for `idx` and `arg` as they + // change on later iterations. + const def_idx = idx; + const def_arg = arg; + deferred_links.push(() => { + const out_data = g.nodes[def_arg.value].data; + let out_port_idx = 0; + if (isAtomicFlowBlockData(out_data)) { + if (out_data.value.options.type !== 'getter') { + out_port_idx++; // Use the second output of the trigger/operation + } + } + + g.edges.push({ + from: { + id: def_arg.value, + output_index: out_port_idx, + }, + to: { + id: id, + input_index: def_idx, + } + }); + }); + continue; + } + + const arg_id = uuidv4(); + deferred_links = deferred_links.concat(add_op_to_graph(arg, toolbox, g, arg_id)); + g.edges.push({ + from: { + id: arg_id, + output_index: 0, + }, + to: { + id: id, + input_index: idx, + } + }); + } + + return deferred_links; +} + +export function compile_spreadsheet(orig: {[key: string]: string}, toolbox: ISpreadsheetToolbox): CompiledFlowGraph[] { + const graph = build_graph(orig, toolbox); + + return compile(graph); +} diff --git a/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.html b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.html new file mode 100644 index 00000000..1374bd25 --- /dev/null +++ b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.html @@ -0,0 +1,198 @@ +
+
+

+ arrow_back_ios + {{program.name}} + Loading... +

+ + + + Scroll to find more buttons + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
    +
  • +

    {{ cat.name }}

    +
      +
    • + {{ block.message }} +
    • +
    +
  • +
+
+
+
+ +
+
+ + + + + + + + + + + + + +
+ {{ _colName(j) }} + +   +
+ {{ i }} + + {{ cellValues[_colName(j) + i] }} +
+
+ +
+
+
+
+
+
diff --git a/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.scss b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.scss new file mode 100644 index 00000000..8a0bafef --- /dev/null +++ b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.scss @@ -0,0 +1,207 @@ +.program-pad #workspace { + width: 100%; +} + +.program-pad #workspace-read-only-marker { + width: 100%; + height: 100%; + position: absolute; + z-index: 999; + background-color: rgba(0,0,0,0.7); +} + +.program-pad #workspace-read-only-marker .message { + position: absolute; + top: 50%; + transform: translateY(-50%); + font-size: 250%; + color: white; + width: 100%; + text-align: center; +} + +.program-pad #workspace-read-only-marker .message button .action-icon { + font-size: inherit; + width: 1em; + vertical-align: sub; +} + +.app-content { + overflow: hidden; +} + +#program-header { + border-bottom: 1px solid #AAA; + overflow-y: auto; + height: 3em; +} + +#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-stop-button, +button#program-logs-button, +button#program-functions-button, +button#advanced-program-controls-button { + vertical-align: top; + padding: 1ex; + margin-left: 0.5ex; + + &.annotated-icon { + padding: 0.25ex; + margin-bottom: 1px; + } +} + +button#program-delete-button{ + float: right; +} + +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: 1ex; +} + +.program-name > a { + padding-left: 1ex; +} + +.program-name > .program-title { + display: inline-block; + width: 15ex; + max-width: 50vw; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.program-name .back-arrow { + vertical-align: middle; +} + +.viewer { + margin: 0; +} + +.spreadsheet-table { + background-color: #fff; + + td[scope="row"], th { + background-color: #f0f0f0; + color: #222; + text-align: center; + min-width: 4rem; + border: 1px solid #ccc; + + &[scope="row"] { + min-width: 4rem; + } + } + td { + border: 1px solid #ccc; + padding: 0.25rem; + min-width: 7rem; + font-size: small; + } + + .col-resize-bar { + width: 5px; + margin-right: -3.5px; // width / 2 + th border + background-color: #ccc; + cursor: col-resize; + float: right; + user-select: none; + + &:hover { + background-color: #BBB; + } + } + + td.active { + border: 2px solid #1a73e8; + } +} + +#floating-editor { + position: absolute; + z-index: 9; + border: 2px solid #1a73e8; + + &.editing { + box-shadow: 0 0 2px 1px #1a73e8; + } + + &.hidden { + display: none; + } +} + +.functions-panel { + .block-list { + padding: 0; + + li { + padding-left: 1rem; + + &:hover { + background-color: #eee; + } + } + } +} diff --git a/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.ts b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.ts new file mode 100644 index 00000000..6fc37e48 --- /dev/null +++ b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-editor.component.ts @@ -0,0 +1,673 @@ +import { isPlatformServer, Location } from '@angular/common'; +import { AfterViewInit, Component, ElementRef, Inject, OnInit, PLATFORM_ID, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatMenu } from '@angular/material/menu'; +import { MatDrawer } from '@angular/material/sidenav'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { ToastrService } from 'ngx-toastr'; +import { Unsubscribable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { environment } from '../../../environments/environment'; +import { BrowserService } from '../../browser.service'; +import { ProgramEditorSidepanelComponent } from '../../components/program-editor-sidepanel/program-editor-sidepanel.component'; +import { ConnectionService } from '../../connection.service'; +import { CustomBlockService } from '../../custom_block.service'; +import { DeleteProgramDialogComponent } from '../../DeleteProgramDialogComponent'; +import { ChangeProgramVisilibityDialog } from '../../dialogs/change-program-visibility-dialog/change-program-visibility-dialog.component'; +import { CloneProgramDialogComponent, CloneProgramDialogComponentData } from '../../dialogs/clone-program-dialog/clone-program-dialog.component'; +import { EnvironmentService } from '../../environment.service'; +import { ProgramContent, ProgramEditorEventValue, SpreadsheetProgram, VisibilityEnum } from '../../program'; +import { ProgramService } from '../../program.service'; +import { SetProgramTagsDialogComponent } from '../../program_tags/SetProgramTagsDialogComponent'; +import { RenameProgramDialogComponent } from '../../RenameProgramDialogComponent'; +import { ServiceService } from '../../service.service'; +import { Session } from '../../session'; +import { SessionService } from '../../session.service'; +import { StopThreadProgramDialogComponent } from '../../StopThreadProgramDialogComponent'; +import { Synchronizer } from '../../syncronizer'; +import * as progbar from '../../ui/progbar'; +import { colName, compile_spreadsheet } from './spreadsheet-compiler'; +import { BlockDef, SpreadsheetToolbox } from './spreadsheet-toolbox'; + + +@Component({ + selector: 'app-my-spreadsheet-editor', + templateUrl: './spreadsheet-editor.component.html', + providers: [ + ConnectionService, CustomBlockService, + ProgramService, ServiceService, SessionService + ], + styleUrls: [ + 'spreadsheet-editor.component.scss', + '../../libs/css/material-icons.css', + '../../libs/css/bootstrap.min.css', + ], +}) +export class SpreadsheetEditorComponent implements OnInit, AfterViewInit { + programId: string; + environment: { [key: string]: any }; + session: Session; + + @ViewChild('drawer') drawer: MatDrawer; + @ViewChild('sidepanel') sidepanel: ProgramEditorSidepanelComponent; + @ViewChild('floatingEditor') floatingEditor: ElementRef; + + program: ProgramContent; + portraitMode: boolean; + smallScreen: boolean; + eventStream: Synchronizer; + connectionLost: boolean; + + read_only: boolean = true; + can_admin: boolean = false; + + // State + cellValues: {[key:string]: string} = {}; + private cursorDiv: HTMLElement; + private cursorInfo: {[key: string]: HTMLElement}; + activeCells: HTMLTableDataCellElement[] = []; + current: HTMLTableDataCellElement; + + // HACK: Prevent the MatMenu import for being removed + private _pinRequiredMatMenuLibrary: MatMenu; + eventSubscription: Unsubscribable | null; + mutationObserver: MutationObserver | null; + visibility: VisibilityEnum; + toolbox: SpreadsheetToolbox; + + // Tools for template + readonly _colName = colName; + + + constructor( + private browser: BrowserService, + private programService: ProgramService, + private customBlockService: CustomBlockService, + private route: ActivatedRoute, + private router: Router, + private _location: Location, + private serviceService: ServiceService, + private notification: MatSnackBar, + private dialog: MatDialog, + private connectionService: ConnectionService, + private sessionService: SessionService, + private environmentService: EnvironmentService, + private toastr: ToastrService, + + @Inject(PLATFORM_ID) private platformId: Object + ) { + this.cursorInfo = {}; + + if (isPlatformServer(this.platformId)) { + // This cannot be rendered on server, so halt it's load + return; + } + } + + ngOnInit(): void { + 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; + + progbar.track(new Promise(async (resolve, reject) => { + + this.session = await this.sessionService.getSession(); + + this.route.params.pipe( + switchMap((params: Params) => { + const user = params['user_id']; + if (user) { + const programName = params['program_id']; + + return this.programService.getProgram(user, programName).catch(err => { + if (!this.session.active) { + this.router.navigate(['/login'], {replaceUrl:true}); + reject(); + this.toastr.error(err.message, "Error loading"); + throw Error("Error loading"); + } + else { + console.error("Error:", err); + this.toastr.error(err.message, "Error loading"); + return null; + } + }); + } + else { + const programId = params['program_id']; + return this.programService.getProgramById(programId).catch(err => { + if (!this.session.active) { + this.router.navigate(['/login'], {replaceUrl:true}); + reject(); + throw Error("Error loading"); + } + else { + console.error("Error:", err); + this.toastr.error(err.message, "Error loading"); + return null; + } + }) + } + })) + .subscribe(program => { + if (program === null) { + return; + } + + this.program = program; + this.programId = program.id; + + this.toolbox = new SpreadsheetToolbox(this.customBlockService, + this.serviceService, + this.environmentService, + this.program.id, + this.connectionService, + this.session, + ); + + this.read_only = program.readonly; + this.visibility = program.visibility; + this.can_admin = program.can_admin; + this.cellValues = program.orig; + if (!this.cellValues || (this.cellValues as any) === 'undefined') { + this.cellValues = {}; + } + resolve(); + }); + })); + } + + 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 }); + } + + initializeListeners() { + this.initializeEventSynchronization(); + } + + initializeEventSynchronization() { + } + + checkpointProgram() { + } + + async reloadToolbox(): Promise { + } + + 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'); + } + } + + reset_zoom() { + } + + reset_header_scroll() { + document.getElementById('program-header').scrollTo(0, 0); + } + + goBack(): boolean { + this.dispose(); + this._location.back(); + return false; + } + + force_reload() { + location = location; + } + + dispose() { + if (this.sidepanel) { + this.sidepanel.dispose(); + } + + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } + } + + async sendProgram(): Promise { + // Serialize result + let program; + try { + const orig = this.cellValues; + const parsed: any[] = compile_spreadsheet(orig, this.toolbox); + program = new SpreadsheetProgram(this.program, + parsed, + orig); + } + catch (error) { + this.toastr.error(error, 'Invalid program', { + closeButton: true, + progressBar: true, + }); + + console.error(error); + return; + } + + // Send update + const button = document.getElementById('program-start-button'); + if (button){ + button.classList.add('started'); + button.classList.remove('completed'); + } + + const result = await this.programService.updateProgram(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.cellValues; + 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}/scratch`], { 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.renameProgram(this.program, programData.name) + .catch(() => { return false; }) + .then((success: boolean) => { + if (!success) { + return; + } + + this.program.name = programData.name; + })); + 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: boolean) => { + if (!success) { + return; + } + + this.notification.open('Tags updated', 'ok', { + duration: 5000 + }); + }) + .catch((error: 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: boolean) => { + if (!success) { + return; + } + this.notification.open('All Threads stopped', 'ok', { + duration: 5000 + }); + })); + progbar.track(stopThreads); + }); + } + + deleteProgram() { + const programData = { name: this.program.name }; + + const dialogRef = this.dialog.open(DeleteProgramDialogComponent, { + data: programData + }); + + dialogRef.afterClosed().subscribe(result => { + if (!result) { + console.log("Cancelled"); + return; + } + + const deletion = (this.programService.deleteProgram(this.program) + .catch(() => { return false; }) + .then((success: boolean) => { + 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(); + } + } + } + + toggleFunctionsPanel() { + if (this.drawer.opened && this.sidepanel.drawerType === 'custom') { + this.closeDrawer(); + } + else { + this.sidepanel.setDrawerType('custom'); + if (!this.drawer.opened) { + this.openDrawer(); + } + } + } + + notifyResize() { + this.browser.window.dispatchEvent(new Event('resize')); + } + + openDrawer() { + return this.drawer.open(); + } + + closeDrawer = () => { + return this.drawer.close(); + } + + onToggleMark = (blockId: string, activate: boolean, message: string) => { + console.log("TODO: Mark block id=", blockId); + } + + // Template helpers + seq(start: number, end: number): number[] { + if (start >= end) { + return []; + } + + return Array.from({length: end - start}, (_v, idx) => idx + start); + } + + startResize(ev: MouseEvent) { + const ref = (ev.target as HTMLElement).parentElement; + window.onmousemove = (move: MouseEvent) => { + ref.style.minWidth = move.clientX - ref.getBoundingClientRect().left + 'px'; + } + + window.onmouseup = () => { + window.onmousemove = window.onmouseup = null; + } + } + + contextMenuOnCell(ev: MouseEvent) { + console.log("Canceling context menu"); + ev.preventDefault(); + } + + mousedownOnCell(ev: MouseEvent) { + const elem = ev.target as HTMLTableDataCellElement; + + ev.preventDefault(); + + if (elem === this.current) { + // Just check that it's being edited + if (!this.floatingEditor.nativeElement.classList.contains('editing')) { + this.startEditing(elem); + } + + return; + } + + const editor = this.floatingEditor.nativeElement; + if (this.current && editor.value.trim().startsWith('=')) { + // Selecting other cell while on formula + const val = this.getCellId(elem); + editor.value = editor.value.substr(0, editor.selectionStart) + + colName(val[0]) + val[1] + + editor.value.substr(editor.selectionStart) + + return; + } + + // Changing the current cell + this.unsetActive(); + elem.classList.add('active'); + this.activeCells = [elem]; + this.makeCurrent(elem); + if (ev.button === 2) { + this.closeEditor(); + this.showOptions(elem); + } + else { + this.startEditing(elem); + } + } + + showOptions(elem: HTMLTableDataCellElement) { + console.log("TODO: Show options for", elem); + } + + keydownOnEditor(ev: KeyboardEvent) { + this.floatingEditor.nativeElement.classList.add('editing'); + if (ev.key === 'Enter') { + this.commitEditor(); + this.closeEditor(); + } + } + + commitEditor() { + const editor = this.floatingEditor.nativeElement; + if (this.current) { + const value = editor.value; + const [col, row] = this.getCellId(this.current); + + this.cellValues[colName(col)+row] = value; + } + editor.value = ''; + } + + closeEditor() { + const editor = this.floatingEditor.nativeElement; + const classes = editor.classList; + classes.remove('editing'); + classes.add('hidden'); + editor.blur(); + } + + makeCurrent(elem: HTMLTableDataCellElement) { + this.current = elem; + } + + startEditing(elem: HTMLTableDataCellElement) { + const toRect = this.current.getBoundingClientRect(); + const editor = this.floatingEditor.nativeElement; + editor.value = elem.innerText; + + const workspace = document.querySelector('.spreadsheet-viewer'); + const wsRect = workspace.getBoundingClientRect(); + + editor.classList.remove('hidden'); + editor.classList.remove('editing'); + + editor.style.left = toRect.left - wsRect.left + 'px'; + editor.style.top = toRect.top - wsRect.top + 'px'; + editor.style.minHeight = toRect.height + 'px'; + editor.style.width = toRect.width + 'px'; + + setTimeout(() => editor.focus(), 0); + } + + getCellId(elem: HTMLTableDataCellElement): [number, number] { + const [_, row, col] = elem.id.split('_'); + + return [parseInt(col), parseInt(row)]; + } + + copyBlock(block: BlockDef) { + console.log("Copying", block); + if (this.current) { + const editor = this.floatingEditor.nativeElement; + editor.value = '=' + block.id + '()'; + editor.selectionEnd = editor.selectionStart = editor.value.length - 1; + editor.scrollIntoView({ block: 'end' }); + editor.focus(); + } + } + + unsetActive() { + if (this.floatingEditor.nativeElement.classList.contains('editing')) { + this.commitEditor(); + } + + for (const cell of this.activeCells) { + cell.classList.remove('active'); + } + } +} diff --git a/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-toolbox.ts b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-toolbox.ts new file mode 100644 index 00000000..a8eeedb9 --- /dev/null +++ b/frontend/src/app/program-editors/spreadsheet-editor/spreadsheet-toolbox.ts @@ -0,0 +1,103 @@ +import { ConnectionService } from "../../connection.service"; +import { CustomBlockService } from "../../custom_block.service"; +import { EnvironmentService } from "../../environment.service"; +import { ServiceService } from "../../service.service"; +import { Session } from "../../session"; +import { BridgeConnection } from "../../connection"; +import { iconDataToUrl } from "../../utils"; +import { ResolvedCustomBlock } from "../../custom_block"; + +export type BlockDef = { + id: string, + message: string, + icon?: string, +}; + +export type CategoryDef = { + id: string, + name: string, + blocks: BlockDef[], +} + +export interface ISpreadsheetToolbox { + categories: CategoryDef[]; + nonEmptyCategories: CategoryDef[]; + blockMap: { [key: string]: { cat: CategoryDef, block: ResolvedCustomBlock } }; +} + +export function simplify_id(block: ResolvedCustomBlock) { + return block.service_port_id.substr(0, 2) + + block.service_port_id.substr(block.service_port_id.length - 2) + + '_' + block.function_name +} + +export class SpreadsheetToolbox { + + public categories: CategoryDef[] = []; + public nonEmptyCategories: CategoryDef[]; + public blockMap: { [key: string]: { cat: CategoryDef, block: ResolvedCustomBlock } } = {}; + + constructor ( + private customBlockService: CustomBlockService, + private serviceService: ServiceService, + private environmentService: EnvironmentService, + private programId: string, + private connectionService: ConnectionService, + private session: Session, + ) { + this.init(); + } + + private async init() { + const availableConnectionsQuery = this.connectionService.getAvailableBridgesForNewConnectionOnProgram(this.programId); + + const [connections, services] = await Promise.all([ + this.connectionService.getConnectionsOnProgram(this.programId), + this.serviceService.getAvailableServicesOnProgram(this.programId), + ]); + + const connection_by_id: {[key: string]: BridgeConnection} = {}; + + for (const connection of connections) { + connection_by_id[connection.bridge_id] = connection; + } + + const blockMap: { [key: string]: { cat: CategoryDef, block: ResolvedCustomBlock } } = {}; + const categories: {id: string, name: string, blocks: BlockDef[]}[] = []; + for (const service of services) { + categories.push({ id: service.id, name: service.name, blocks: [] }); + } + + const skip_resolve_argument_options = true; // Enum options will be filled when needed + const blocks = await this.customBlockService.getCustomBlocksOnProgram(this.programId, skip_resolve_argument_options); + for (const block of blocks) { + let icon: string | null = null; + + const connection = connection_by_id[block.service_port_id]; + if (connection) { + icon = iconDataToUrl(this.environmentService, connection.icon, connection.bridge_id); + } + else { + console.error("No connection found for", block, connection_by_id); + } + + const category = categories.find(c => c.id === block.service_port_id); + if (!category) { + console.error("Ignoring block. Category not found for", block, block.service_port_id); + continue; + } + + const reducedId = simplify_id(block); + category.blocks.push({ + icon: icon, + message: block.message, + id: reducedId + }); + blockMap[reducedId] = { cat: category, block: block }; + } + + this.categories = categories; + this.nonEmptyCategories = this.categories.filter(cat => cat.blocks.length > 0); + this.blockMap = blockMap; + } +} diff --git a/frontend/src/app/program-transformations.ts b/frontend/src/app/program-transformations.ts new file mode 100644 index 00000000..568cd675 --- /dev/null +++ b/frontend/src/app/program-transformations.ts @@ -0,0 +1,294 @@ +import { FlowGraphNode, FlowGraph } from './flow-editor/flow_graph'; +import { ProgramContent } from './program'; + +export class NoTranslationFoundError extends Error {} +const TIME_BRIDGE = "0093325b-373f-4f1c-bace-4532cce79df4"; + +export function getRequiredAssets(programData: ProgramContent): string[] { + if (programData.type !== 'flow_program') { + return []; + } + + const assets: {[key: string]: boolean} = {}; + + // Transform graph + const graph: FlowGraph = programData.orig; + for (const node_id of Object.keys(graph.nodes)) { + const node = graph.nodes[node_id]; + + // Only simple_flow_blocks have to be transformed + if (node.data.type !== 'ui_flow_block') { + continue; + } + + const ids = getFlowBlockIds(node); + + for (const id of ids) { + assets[id] = true; + } + } + + return Object.keys(assets); +} + +function getBlockRequiredBridges(blocks: any[], required: {[key: string]: boolean}) { + for (const block of blocks) { + if (block.contents && block.contents.length > 0) { + getBlockRequiredBridges(block.contents, required); + } + + let args = []; + + if (block.args instanceof Array) { + args = block.args; + } + else if (block.args['service_call_values']) { + args = block.args['service_call_values']; + } + + for (const arg of args || []) { + if (arg.type === 'block') { + getBlockRequiredBridges(arg.value, required); + } + } + + const fun = block.type; + + + if (fun === 'command_call_service') { + const origBridge = block.args.service_id; + required[origBridge] = true; + } + else if ((!fun) || (!fun.startsWith('services.')) || (fun.startsWith('services.ui.'))) { + continue; + } + else { + const chunks = fun.split('.'); + + const origBridge = chunks[1]; + required[origBridge] = true; + } + } +} + +// Destructively transform the program so it can be applied to the target user +export function getRequiredBridges(programData: ProgramContent): string[] { + + const required: {[key: string]: boolean} = {}; + + // Transform editor representation + if (programData.type === 'flow_program') { + // Transform graph + const graph: FlowGraph = programData.orig; + for (const node_id of Object.keys(graph.nodes)) { + const node = graph.nodes[node_id]; + + // Only simple_flow_blocks have to be transformed + if (node.data.type !== 'simple_flow_block') { + continue; + } + const fun = node.data.value.options.block_function; + + if ((!fun) || (!fun.startsWith('services.')) || (fun.startsWith('services.ui.'))) { + continue; + } + + const chunks = fun.split('.'); + + const origBridge = chunks[1]; + required[origBridge] = true; + } + } + else if (programData.type === 'scratch_program') { + const parser = new DOMParser(); + const serializer = new XMLSerializer(); + + const xml = parser.parseFromString(programData.orig, 'text/xml'); + const blocks = xml.getElementsByTagName("block"); + for (const block of Array.from(blocks)) { + const fun = block.getAttribute("type"); + + if ((!fun) || (!fun.startsWith('services.')) || (fun.startsWith('services.ui.'))) { + continue; + } + + const chunks = fun.split('.'); + + const origBridge = chunks[1]; + required[origBridge] = true; + } + } + else { + throw new Error(`Translation of program type not supported: ${programData.type}`) + } + + + // Transform parsed structure + // Maybe just recompiling the graph would be more robust + for (const col of programData.parsed.blocks) { + getBlockRequiredBridges(col, required); + } + + return Object.keys(required); +} + +function getFlowBlockIds(node: FlowGraphNode): string[] { + const ids = []; + + const extra = node.data.value.extra; + if (extra) { + if (extra.settings) { + if (extra.settings.body) { + if (extra.settings.body.image) { + if (extra.settings.body.image.id) { + ids.push(extra.settings.body.image.id); + } + } + } + } + } + + return ids; +} + +function transformBlocks(blocks: any[], translations: {[key: string]: string}) { + for (const block of blocks) { + if (block.contents && block.contents.length > 0) { + transformBlocks(block.contents, translations); + } + + let args = []; + + if (block.args instanceof Array) { + args = block.args; + } + else if (block.args['service_call_values']) { + args = block.args['service_call_values']; + } + + for (const arg of args || []) { + if (arg.type === 'block') { + transformBlocks(arg.value, translations); + } + } + + const fun = block.type; + + if (fun === 'command_call_service') { + const origBridge = block.args.service_id; + const translated = translations[origBridge]; + if (!translated) { + throw new NoTranslationFoundError(origBridge) + } + + console.debug(`[Program] On ${fun}: [${origBridge} -> ${translated}]`); + block.args.service_id = translated; + } + else if ((!fun) || (!fun.startsWith('services.')) || (fun.startsWith('services.ui.'))) { + continue; + } + else { + const chunks = fun.split('.'); + + const origBridge = chunks[1]; + const translated = translations[origBridge]; + if (!translated) { + throw new NoTranslationFoundError(origBridge) + } + + console.debug(`[Program] On ${fun}: [${origBridge} -> ${translated}]`); + chunks[1] = translated; + + block.type = chunks.join('.'); + } + } +} + +// Destructively transform the program so it can be applied to the target user +export function transformProgram(programData: ProgramContent, translations: { [key: string]: string }) { + + // By default the time bridge maps to itself + if (!translations[TIME_BRIDGE]) { + translations[TIME_BRIDGE] = TIME_BRIDGE; + } + + // Transform editor representation + if (programData.type === 'flow_program') { + // Transform graph + const graph: FlowGraph = programData.orig; + for (const node_id of Object.keys(graph.nodes)) { + const node = graph.nodes[node_id]; + + // Only simple_flow_blocks have to be transformed + if (node.data.type !== 'simple_flow_block') { + continue; + } + const fun = node.data.value.options.block_function; + + if ((!fun) || (!fun.startsWith('services.')) || (fun.startsWith('services.ui.'))) { + continue; + } + + const chunks = fun.split('.'); + + const origBridge = chunks[1]; + const translated = translations[origBridge]; + if (!translated) { + throw new Error(`Cannot find translation for bridge id=${origBridge}.`) + } + + console.debug(`[Flow] On ${fun}: [${origBridge} -> ${translated}]`); + chunks[1] = translated; + node.data.value.options.block_function = chunks.join('.'); + if (node.data.value.icon) { + node.data.value.icon.replace(origBridge, translated); + } + } + } + else if (programData.type === 'scratch_program') { + const parser = new DOMParser(); + const serializer = new XMLSerializer(); + + const xml = parser.parseFromString(programData.orig, 'text/xml'); + const blocks = xml.getElementsByTagName("block"); + for (const block of Array.from(blocks)) { + const fun = block.getAttribute("type"); + + if ((!fun) || (!fun.startsWith('services.')) || (fun.startsWith('services.ui.'))) { + continue; + } + + const chunks = fun.split('.'); + + const origBridge = chunks[1]; + const translated = translations[origBridge]; + if (!translated) { + throw new Error(`Cannot find translation for bridge id=${origBridge}.`) + } + + chunks[1] = translated; + block.setAttribute('type', chunks.join('.')); + } + programData.orig = serializer.serializeToString(xml); + } + else { + throw new Error(`Translation of program type not supported: ${programData.type}`) + } + + + // Transform parsed structure + // Maybe just recompiling the graph would be more robust + try { + for (const col of programData.parsed.blocks) { + transformBlocks(col, translations); + } + } + catch (err) { + if (err instanceof NoTranslationFoundError) { + const origBridge = err.message; + throw new Error(`Cannot find translation for bridge id=${origBridge}.`); + } + throw err; + } + +} diff --git a/frontend/src/app/program.service.ts b/frontend/src/app/program.service.ts index af6ae843..74183609 100644 --- a/frontend/src/app/program.service.ts +++ b/frontend/src/app/program.service.ts @@ -1,22 +1,22 @@ - -import {map} from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { ProgramMetadata, ProgramContent, ProgramType } from './program'; -import * as API from './api-config'; - - import { HttpClient } from '@angular/common/http'; -import { SessionService } from './session.service'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, share } from 'rxjs/operators'; +import { SharedResource } from './bridges/bridge'; import { ContentType } from './content-type'; +import { EnvironmentService } from './environment.service'; +import { ProgramContent, ProgramEditorEvent, ProgramEditorEventValue, ProgramInfoUpdate, ProgramLogEntry, ProgramMetadata, ProgramType, VisibilityEnum } from './program'; +import { SessionService } from './session.service'; +import { Synchronizer } from './syncronizer'; +import { addTokenQueryString, toWebsocketUrl } from './utils'; + @Injectable() export class ProgramService { - private getExamplesForProgramRootUrl = '/api/programs/examples/'; - private addExampleToProgramRootUrl = '/api/programs/examples/'; - constructor( private http: HttpClient, - private sessionService: SessionService + private sessionService: SessionService, + private environmentService: EnvironmentService, ) { this.http = http; this.sessionService = sessionService; @@ -32,146 +32,520 @@ export class ProgramService { return userApiRoot + '/programs/'; } - async getRetrieveProgramUrl(_user_id: string, program_id: string) { - const userApiRoot = await this.sessionService.getUserApiRoot(); - return userApiRoot + '/programs/' + program_id; + private getGroupCreateProgramsUrl(groupId: string) { + return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}/programs`; } - async getUpdateProgramUrl(programUserName: string, program_id: string) { - const userApiRoot = await this.sessionService.getApiRootForUser(programUserName); - return userApiRoot + '/programs/' + encodeURIComponent(program_id); + private listProgramsOnGroupUrl(groupId: string) { + return `${this.environmentService.getApiRoot()}/groups/by-id/${groupId}/programs`; } - async getProgramTagsUrl(programUserId: string, program_id: string) { - const userApiRoot = await this.sessionService.getApiRootForUserId(programUserId); - return userApiRoot + '/programs/id/' + encodeURIComponent(program_id) + '/tags'; + private getRetrieveProgramUrl(userId: string, programName: string) { + return `${this.environmentService.getApiRoot()}/users/${userId}/programs/${programName}`; } - async getProgramStopThreadsUrl(programUserId: string, program_id: string) { - const userApiRoot = await this.sessionService.getApiRootForUserId(programUserId); - return userApiRoot + '/programs/id/' + encodeURIComponent(program_id) + '/stop-threads'; + private getRetrieveProgramUrlById(programId: string): string { + return this.environmentService.getApiRoot() + '/programs/by-id/' + programId; } - async getProgramsStatusUrl(programUserId: string, program_id: string) { - const userApiRoot = await this.sessionService.getApiRootForUserId(programUserId); - return userApiRoot + '/programs/id/' + encodeURIComponent(program_id) + '/status'; + async getUpdateProgramUrl(programId: string) { + return this.environmentService.getApiRoot() + '/programs/by-id/' + programId; } - getPrograms(): Promise { - return this.getListProgramsUrl().then(url => - this.http.get(url, {headers: this.sessionService.getAuthHeader()}).pipe( - map(response => response as ProgramMetadata[])) - .toPromise()); + async getProgramCheckpointUrlById(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/checkpoint`; + } + + getProgramSharedResourcesUrl(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/shared-resources`; + } + + async getProgramTagsUrl(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/tags`; + } + + private async getProgramLogsUrl(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/logs`; + } + + private async getProgramVariablesUrl(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/variables`; + } + + private async getProgramVariableUrl(programId: string, name: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/variables/${name}`; + } + + private async getProgramStreamingLogsUrl(programId: string) { + const token = this.sessionService.getToken(); + return addTokenQueryString(toWebsocketUrl(this.environmentService, + `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/logs-stream`), + token, + ); + } + + private async getProgramStreamingVariablesUrl(programId: string) { + const token = this.sessionService.getToken(); + return addTokenQueryString(toWebsocketUrl(this.environmentService, + `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/variables-stream`), + token, + ); } - getProgram(user_id: string, program_id: string): Promise { - return this.getRetrieveProgramUrl(user_id, program_id).then(url => + private getProgramStreamingEventsUrl(programId: string) { + const token = this.sessionService.getToken(); + return addTokenQueryString(toWebsocketUrl(this.environmentService, + `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/editor-events`), + token, + ); + } + + async getProgramStopThreadsUrl(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/stop-threads`; + } + + async getProgramsStatusUrl(programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/status`; + } + getAssetUrlOnProgram(assetId: string, programId: string): string { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/assets/by-id/${assetId}`; + } + + getPrograms(): Promise { + return this.getListProgramsUrl().then(url => this.http.get(url, {headers: this.sessionService.getAuthHeader()}).pipe( - map(response => response as ProgramContent)) + map(response => response as ProgramMetadata[])) .toPromise()); } - getProgramTags(user_id: string, program_id: string): Promise { - return this.getProgramTagsUrl(user_id, program_id).then(url => + async getProgramsOnGroup(groupId: string): Promise { + const url = this.listProgramsOnGroupUrl(groupId) + + const result = await this.http.get(url, {headers: this.sessionService.getAuthHeader()}).toPromise(); + + return (result as any)['programs']; + } + + async getProgram(userName: string, programName: string): Promise { + const url = this.getRetrieveProgramUrl(userName, programName) + + const result = await this.http.get(url, {headers: this.sessionService.getAuthHeader()}).toPromise(); + + return result as ProgramContent; + } + + async getProgramById(programId: string): Promise { + const url = this.getRetrieveProgramUrlById(programId); + return (this.http.get(url, {headers: this.sessionService.getAuthHeader()}) + .toPromise() as Promise); + } + + getProgramTags(programId: string): Promise { + return this.getProgramTagsUrl(programId).then(url => this.http.get(url, {headers: this.sessionService.getAuthHeader()}).pipe( map(response => response as string[])) .toPromise()); } - createProgram(): Promise { - return this.getCreateProgramsUrl().then(url => - this.http - .post(url, JSON.stringify({}), - {headers: this.sessionService.addJsonContentType( - this.sessionService.getAuthHeader())}).pipe( - map(response => { - return response as ProgramMetadata; - })) - .toPromise()); + getProgramLogs(programId: string): Promise { + return (this.getProgramLogsUrl(programId) + .then(url => + this.http.get(url, {headers: this.sessionService.getAuthHeader()}) + .toPromise()) as Promise); } - updateProgram(username: string, - program: ProgramContent): Promise { - return this.getUpdateProgramUrl(username, program.name).then(url => + async getProgramVariables(programId: string): Promise<{ [key: string]: any }> { + const url = await this.getProgramVariablesUrl(programId); + const result = await this.http.get(url, {headers: this.sessionService.getAuthHeader()}).toPromise(); + + return (result as any)['variables']; + } + + async updateProgramVariables(programId: string, values: { name: string, value: any }[]): Promise { + const url = await this.getProgramVariablesUrl(programId); + const result = await this.http.patch(url, + { values: values }, + { headers: this.sessionService.getAuthHeader() } + ).toPromise(); + } + + async removeVariable(programId: string, name: string): Promise { + const url = await this.getProgramVariableUrl(programId, name); + const result = await this.http.delete(url, + { headers: this.sessionService.getAuthHeader() } + ).toPromise(); + } + + public async createProgram(programType?: ProgramType, programName?: string): Promise { + const data: { type?: string, name?: string } = {}; + if (programType) { + data.type = programType; + } + if (programName) { + data.name = programName; + } + + const url = await this.getCreateProgramsUrl(); + return await this.http + .post(url, JSON.stringify(data), { + headers: this.sessionService.addJsonContentType(this.sessionService.getAuthHeader()) + }).toPromise() as Promise; + } + + public async createProgramOnGroup(programType: ProgramType, programName: string | null, groupId: string): Promise { + const data: { type?: string, name?: string } = {}; + if (programType) { + data.type = programType; + } + if (programName) { + data.name = programName; + } + + const url = this.getGroupCreateProgramsUrl(groupId); + return await this.http + .post(url, JSON.stringify(data), { + headers: this.sessionService.addJsonContentType(this.sessionService.getAuthHeader()) + }).toPromise() as Promise; + } + + updateProgram(program: ProgramContent): Promise { + return this.getUpdateProgramUrl(program.id).then(url => this.http .put(url, JSON.stringify({type: program.type, orig: program.orig, parsed: program.parsed}), {headers: this.sessionService.addContentType( this.sessionService.getAuthHeader(), - ContentType.Json)}).pipe( - map(response => { - return true; - })) + ContentType.Json)}) + .pipe(map(_ => true)) .toPromise() .catch(_ => false) ); } - updateProgramTags(user_id: string, program_id: string, programTags: string[]): Promise { - return this.getProgramTagsUrl(user_id, program_id).then(url => + async updateProgramById(program: { id: string, type: ProgramType, orig: any, parsed: any, pages?: {[key: string]: any} }): Promise { + const url = await this.getUpdateProgramUrl(program.id); + + const data = {type: program.type, orig: program.orig, parsed: program.parsed, pages: program.pages}; + + try { + (await + this.http + .put(url, + JSON.stringify(data), + {headers: this.sessionService.addContentType( + this.sessionService.getAuthHeader(), + ContentType.Json)}) + .toPromise()); + return true; + } + catch (err) { + console.error("Error updating program:", err); + return false; + } + } + + updateProgramTags(programId: string, programTags: string[]): Promise { + return this.getProgramTagsUrl(programId).then(url => this.http .post(url, JSON.stringify({tags: programTags}), {headers: this.sessionService.addContentType( this.sessionService.getAuthHeader(), - ContentType.Json)}).pipe( - map(response => { - return true; - })) + ContentType.Json)}) + .pipe(map(_ => true)) .toPromise() .catch(_ => false) ); } - renameProgram(username: string, program: ProgramContent, new_name: string): Promise { - return this.getUpdateProgramUrl(username, program.name).then( + renameProgram(program: ProgramContent, newName: string): Promise { + return this.getUpdateProgramUrl(program.id).then( url => (this.http .patch(url, - JSON.stringify({name: new_name}), + JSON.stringify({name: newName}), {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), - ContentType.Json)}).pipe( - map(response => { - console.log("R:", response); - return true; - })) - .toPromise())); + ContentType.Json)}) + .pipe(map(_ => true)) + .toPromise())); } - stopThreadsProgram(user_id: string, program_id: string): Promise { - return this.getProgramStopThreadsUrl(user_id, program_id).then( + async renameProgramById(programId: string, newName: string): Promise { + const url = await this.getUpdateProgramUrl(programId); + const _response = await (this.http + .patch(url, + JSON.stringify({name: newName}), + {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), + ContentType.Json)}) + .toPromise()); + return true; + } + + stopThreadsProgram(programId: string): Promise { + return this.getProgramStopThreadsUrl(programId).then( url => (this.http .post(url,"", {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), ContentType.Json)}).pipe( map(response => { - console.log("R:", response); return true; })) .toPromise())); } - setProgramStatus(status:string, program_id:string, user_id:string){ - return this.getProgramsStatusUrl(user_id, program_id).then( + setProgramStatus(status:string, programId:string){ + return this.getProgramsStatusUrl(programId).then( url => (this.http .post(url, status, {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), - ContentType.Json)}).pipe( - map(response => { - console.log("R:", response); - return true; - })) - .toPromise())); + ContentType.Json)}) + .pipe(map(_ => true)) + .toPromise())); } - deleteProgram(username: string, program: ProgramContent): Promise { - return this.getUpdateProgramUrl(username, program.name).then( + updateProgramVisibility(programId: string, options: { visibility: VisibilityEnum }){ + return this.getUpdateProgramUrl(programId).then( + url => (this.http + .patch(url, + JSON.stringify({visibility: options.visibility}), + {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), + ContentType.Json)}) + .pipe(map(_ => true)) + .toPromise())); + } + + deleteProgram(program: ProgramContent): Promise { + return this.getUpdateProgramUrl(program.id).then( url => (this.http .delete(url, {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), - ContentType.Json)}).pipe( - map(response => { - console.log("R:", response); - return true; - })) - .toPromise())); + ContentType.Json)}) + .pipe(map(_ => true)) + .toPromise())); + } + + async deleteProgramById(programId: string): Promise { + const url = await this.getUpdateProgramUrl(programId); + const _response = await(this.http + .delete(url, + {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), + ContentType.Json)}) + .toPromise()); + return true; + } + + async checkpointProgram(programId: string, content: any): Promise { + const url = await this.getProgramCheckpointUrlById(programId); + const _response = await( + this.http + .post(url, + JSON.stringify(content), + {headers: this.sessionService.addContentType(this.sessionService.getAuthHeader(), + ContentType.Json)}) + .toPromise()); + } + + public getPageUrl(programId: string, pagePath: string): any { + let baseServerPath = document.location.origin; + const apiHost = this.environmentService.getBrowserApiHost(); + if (apiHost != '') { + baseServerPath = apiHost; + } + + return `${baseServerPath}/api/v0/programs/by-id/${programId}/render${pagePath}`; + } + + async getProgramSharedResources(programId: string): Promise { + const url = this.getProgramSharedResourcesUrl(programId); + + const response = await (this.http + .get(url, + {headers: this.sessionService.getAuthHeader()}) + .toPromise()); + + return (response as any)['resources'] as SharedResource[]; + } + + + watchProgramLogs(programId: string, options: { request_previous_logs?: boolean }): Observable { + let websocket: WebSocket | null = null; + + return new Observable((observer) => { + + this.getProgramStreamingLogsUrl(programId).then(streamingUrl => { + + let buffer: any[] = []; + let state : 'none_ready' | 'ws_ready' | 'all_ready' = 'none_ready'; + + websocket = new WebSocket(streamingUrl); + websocket.onopen = (() => { + if (options.request_previous_logs) { + state = 'ws_ready'; + + this.getProgramLogs(programId).then(entries => { + for (const entry of entries) { + observer.next({ + type: 'program_log', + value: entry, + }); + } + + for (const entry of buffer) { + observer.next(entry); + } + + buffer = []; // Empty buffer + state = 'all_ready'; + }); + } + else { + state = 'all_ready'; + } + }); + + websocket.onmessage = ((ev) => { + if (state === 'ws_ready') { + buffer.push(JSON.parse(ev.data)); + } + else { + observer.next(JSON.parse(ev.data)); + } + }); + + websocket.onclose = (() => { + observer.complete(); + }); + + websocket.onerror = ((ev) => { + observer.error(ev); + observer.complete(); + }); + }); + + return () => { + if (websocket) { + websocket.close(); + } + } + }); + } + + watchProgramVariables(programId: string, options: { request_previous?: boolean }): Observable<{ name: string, value: any }> { + let websocket: WebSocket | null = null; + + return new Observable((observer) => { + + this.getProgramStreamingVariablesUrl(programId).then(streamingUrl => { + + let buffer: any[] = []; + let state : 'none_ready' | 'ws_ready' | 'all_ready' = 'none_ready'; + + websocket = new WebSocket(streamingUrl); + websocket.onopen = (() => { + if (options.request_previous) { + state = 'ws_ready'; + + this.getProgramVariables(programId).then(entries => { + for (const name of Object.keys(entries)) { + observer.next({ name: name, value: entries[name]}); + } + + for (const entry of buffer) { + observer.next(entry); + } + + buffer = []; // Empty buffer + state = 'all_ready'; + }); + } + else { + state = 'all_ready'; + } + }); + + websocket.onmessage = ((ev) => { + if (state === 'ws_ready') { + buffer.push(JSON.parse(ev.data)); + } + else { + observer.next(JSON.parse(ev.data)); + } + }); + + websocket.onclose = (() => { + observer.complete(); + }); + + websocket.onerror = ((ev) => { + observer.error(ev); + observer.complete(); + }); + }); + + return () => { + if (websocket) { + websocket.close(); + } + } + }); + } + + getEventStream(programId: string, opts: { skip_previous?: boolean } = {}): Synchronizer { + let websocket: WebSocket | null = null; + let sendBuffer: {type: string, value: ProgramEditorEventValue}[] = []; + let state : 'none_ready' | 'ws_ready' | 'all_ready' | 'closed' = 'none_ready'; + + const obs = new Observable((observer) => { + let streamingUrl = this.getProgramStreamingEventsUrl(programId); + if (opts.skip_previous) { + streamingUrl += '&skip_previous=true'; + } + + if (state === 'closed') { + return; // Cancel the opening of websocket if the stream was closed before being established + } + + websocket = new WebSocket(streamingUrl) + + websocket.onopen = (() => { + state = 'all_ready'; + for (const ev of sendBuffer) { + websocket.send(JSON.stringify(ev)); + } + sendBuffer = null; + }); + + websocket.onmessage = ((ev) => { + const parsed: ProgramEditorEvent = JSON.parse(ev.data); + observer.next(parsed.value); + }); + + websocket.onclose = (() => { + observer.complete(); + }); + + websocket.onerror = ((ev) => { + observer.error(ev); + observer.complete(); + }); + + return () => { + if (websocket) { + websocket.close(); + } + } + }); + + const sharedObserver = obs.pipe(share()); + return { + subscribe: sharedObserver.subscribe.bind(sharedObserver), + push: (ev: ProgramEditorEventValue) => { + const msg = {type: 'editor_event', value: ev}; + if (state === 'none_ready') { + sendBuffer.push(msg); + } + else { + websocket.send(JSON.stringify(msg)); + } + } + }; } } diff --git a/frontend/src/app/program.ts b/frontend/src/app/program.ts index 772fb81d..2a799aa0 100644 --- a/frontend/src/app/program.ts +++ b/frontend/src/app/program.ts @@ -1,23 +1,37 @@ +import { CompiledFlowGraph } from "./flow-editor/flow_graph"; + +export type VisibilityEnum = 'public' | 'shareable' | 'private'; + export class ProgramMetadata { id: string; name: string; - link: string; enabled: boolean; + visibility: VisibilityEnum; + type: string; + bridges_in_use: string[]; } -export type ProgramType = 'scratch_program'; +export type ProgramType = 'scratch_program' + | 'flow_program' + | 'spreadsheet_program' // No longer created through UI +; +export type OwnerType = 'user' | 'group'; export class ProgramContent extends ProgramMetadata { type: ProgramType; parsed: any; orig: any; owner: string; + owner_full: { type: OwnerType, id: string}; + checkpoint?: any; + + "readonly"?: boolean; + can_admin?: boolean; constructor (metadata: ProgramMetadata, parsed: any, orig: any, type: ProgramType) { super(); this.id = metadata.id; - this.link = metadata.link; this.name = metadata.name; this.parsed = parsed; @@ -31,3 +45,34 @@ export class ScratchProgram extends ProgramContent { super(metadata, parsed, orig, 'scratch_program'); } } + +export class FlowProgram extends ProgramContent { + constructor(metadata: ProgramMetadata, parsed: any, orig: any) { + super(metadata, parsed, orig, 'flow_program'); + } +} + +export class SpreadsheetProgram extends ProgramContent { + constructor(metadata: ProgramMetadata, parsed: CompiledFlowGraph, orig: any) { + super(metadata, { blocks: parsed, variables: []}, orig, 'spreadsheet_program'); + } +} + +export interface ProgramLogEntry { + program_id: string, + thread_id: string | 'none', + user_id: string | 'none', + block_id: string | undefined, + severity: 'error' | 'debug' | 'warning', + event_data: any, + event_message: string, + event_time: number, +}; + +export type ProgramInfoUpdate = { type: "program_log" | "debug_log", value: ProgramLogEntry }; + +export type ProgramEditorEventType = 'blockly_event' | 'flow_event' | 'cursor_event' | 'save_checkpoint' | 'add_editor' | 'remove_editor' | 'ready'; + +export type ProgramEditorEventValue = { type: ProgramEditorEventType, value: any, save?: boolean }; + +export type ProgramEditorEvent = { type: "editor_event", value: ProgramEditorEventValue }; diff --git a/frontend/src/app/program_serialization/scratch-program-serializer.ts b/frontend/src/app/program_serialization/scratch-program-serializer.ts index 9b582066..3a65bd78 100644 --- a/frontend/src/app/program_serialization/scratch-program-serializer.ts +++ b/frontend/src/app/program_serialization/scratch-program-serializer.ts @@ -1,4 +1,7 @@ import { ToolboxController } from "../blocks/ToolboxController"; +import { cleanCallbackSequenceValue } from "app/blocks/CallbackSequenceField"; + +type CompiledScratchBlock = { id: string, type: string, args: any[] | {[key: string]: any}, contents: any[], save_to?: { index: number } }; export default class ScratchProgramSerializer { // ==================================================== @@ -15,7 +18,7 @@ export default class ScratchProgramSerializer { try { const serialized = { variables: this.serializeVariables(variables as HTMLElement), - blocks: blocks.map(block => this.serializeBlock(block as HTMLElement)), + blocks: blocks.map(block => this.serializeColumn(block as HTMLElement)).filter(column => !!column), }; return { @@ -32,16 +35,30 @@ export default class ScratchProgramSerializer { // ==================================================== // Internal functions // ==================================================== + private serializeColumn(block: HTMLElement): any { + const blockType = ScratchProgramSerializer.cleanTypeName(block.getAttribute('type')); + const knownBlock = this.toolboxController.customBlocks[blockType]; + + if (knownBlock && knownBlock.block_type !== 'trigger') { + return null; // This column cannot be executed + } + + return this.serializeBlock(block); + } + private serializeBlock(block: HTMLElement, chain: any[] = null): any { if (chain === null) { chain = []; } - let cleanedElement = { + let cleanedElement: CompiledScratchBlock = { id: block.id, type: ScratchProgramSerializer.cleanTypeName(block.getAttribute('type')), args: Array.from(block.childNodes) - .filter((x: HTMLElement) => x.tagName !== 'NEXT' && x.tagName !== 'STATEMENT') + .filter((x: HTMLElement) => (x.tagName !== 'NEXT' + && x.tagName !== 'STATEMENT' + && x.tagName !== 'COMMENT' + )) .map(node => this.serializeArg(node as HTMLElement)), contents: [], }; @@ -54,7 +71,6 @@ export default class ScratchProgramSerializer { const contents = Array.from(block.childNodes).filter((x: HTMLElement) => x.tagName === 'STATEMENT'); if (contents.length == 1) { - console.log("Contents:", contents); cleanedElement.contents = this.serializeBlock(contents[0].firstChild as HTMLElement); } else if (contents.length > 0) { @@ -76,8 +92,8 @@ export default class ScratchProgramSerializer { return chain; } - private fixArgOrder(cleanedElement: { args: any[] }) { - const order = cleanedElement.args.map((value) => { + private fixArgOrder(cleanedElement: CompiledScratchBlock) { + const order = (cleanedElement.args as any[]).map((value) => { if (!value.name) { return null; } @@ -113,10 +129,9 @@ export default class ScratchProgramSerializer { return parseInt(match[0]); } - private rewriteCustomBlock(element) { + private rewriteCustomBlock(element: CompiledScratchBlock) { const blockInfo = this.toolboxController.getBlockInfo(element.type); if (!blockInfo) { - return element; } @@ -131,12 +146,12 @@ export default class ScratchProgramSerializer { return element; } - private rewriteCustomTrigger(element, blockInfo) { + private rewriteCustomTrigger(element: CompiledScratchBlock, blockInfo: any) { const args: any = {}; if (blockInfo.save_to) { let save_to = null; if (blockInfo.save_to.type === 'argument') { - save_to = { 'type': 'variable', 'value': element.args[blockInfo.save_to.index].value }; + save_to = { 'type': 'variable', 'value': (element.args as any[])[blockInfo.save_to.index].value }; } args.monitor_save_value_to = save_to; @@ -145,7 +160,7 @@ export default class ScratchProgramSerializer { if (blockInfo.expected_value) { let expected_value = null; if (blockInfo.expected_value.type === 'argument') { - expected_value = element.args[blockInfo.expected_value.index]; + expected_value = (element.args as any[])[blockInfo.expected_value.index]; } args.monitor_expected_value = expected_value; @@ -158,7 +173,7 @@ export default class ScratchProgramSerializer { if (blockInfo.subkey.type === 'argument') { args.subkey = { 'type': 'constant', - 'value': element.args[blockInfo.subkey.index].value, + 'value': (element.args as any[])[blockInfo.subkey.index].value, }; } else { @@ -171,11 +186,11 @@ export default class ScratchProgramSerializer { return element; } - private replaceServices(element) { + private replaceServices(element: CompiledScratchBlock) { } - private replaceMonitors(element) { + private replaceMonitors(element: CompiledScratchBlock) { switch (element.type) { case "time_trigger_at": // This implies a call to a monitor @@ -185,14 +200,32 @@ export default class ScratchProgramSerializer { "monitor_id": { "from_service": "0093325b-373f-4f1c-bace-4532cce79df4" }, // Timekeeping monitor ID "monitor_expected_value": { "type": "constant", - "value": (element.args[0].value.replace(/^0*(\d)$/, "$1") + ':' - + element.args[1].value.replace(/^0*(\d)$/, "$1") + ':' - + element.args[2].value.replace(/^0*(\d)$/, "$1") + "value": ((element.args as any[])[0].value.replace(/^0*(\d)$/, "$1") + ':' + + (element.args as any[])[1].value.replace(/^0*(\d)$/, "$1") + ':' + + (element.args as any[])[2].value.replace(/^0*(\d)$/, "$1") ) } } break; } + + case "time_trigger_at_tz": + // This implies a call to a monitor + { + element.type = "wait_for_monitor"; + element.args = { + "monitor_id": { "from_service": "0093325b-373f-4f1c-bace-4532cce79df4" }, // Timekeeping monitor ID + "monitor_expected_value": { + "type": "constant", + "value": ((element.args as any[])[0].value.replace(/^0*(\d)$/, "$1") + ':' + + (element.args as any[])[1].value.replace(/^0*(\d)$/, "$1") + ':' + + (element.args as any[])[2].value.replace(/^0*(\d)$/, "$1") + ) + }, + "timezone": (element.args as any[])[3].value, + } + break; + } } } @@ -204,10 +237,16 @@ export default class ScratchProgramSerializer { private serializeArg(argument: HTMLElement): any { if (argument.tagName === 'FIELD') { let type = argument.getAttribute('name').toLowerCase(); + let value = argument.innerText; if (type.startsWith('val')) { type = "constant"; } + else if (type.startsWith('cbsequence')) { + // Additional modifications must be done on this blocks + type = "constant"; + value = cleanCallbackSequenceValue(value); + } if (argument.getAttribute('id') === null) { type = 'constant'; // No block or value, but dropdown/constant } @@ -215,7 +254,7 @@ export default class ScratchProgramSerializer { name: argument.getAttribute('name'), // Type here might be 'constant', 'variable' or 'list' type: ScratchProgramSerializer.cleanTypeName(type), - value: argument.innerText, + value: value, } } diff --git a/frontend/src/app/program_tags/SetProgramTagsDialogComponent.ts b/frontend/src/app/program_tags/SetProgramTagsDialogComponent.ts index 0e75fe29..08b6db5f 100644 --- a/frontend/src/app/program_tags/SetProgramTagsDialogComponent.ts +++ b/frontend/src/app/program_tags/SetProgramTagsDialogComponent.ts @@ -5,9 +5,6 @@ import { FormControl } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; - import { ProgramMetadata } from '../program'; import { ProgramService } from '../program.service'; import { SessionService } from '../session.service'; @@ -54,7 +51,7 @@ export class SetProgramTagsDialogComponent { this.programService = programService; data.tags = this.tags; - this.programService.getProgramTags(this.programUserId, this.program.id).then((tags) => { + this.programService.getProgramTags(this.program.id).then((tags) => { // Bind data.tags again now that this.tags has changed data.tags = this.tags = tags; }); @@ -83,7 +80,7 @@ export class SetProgramTagsDialogComponent { // Remove duplicated tags deduplicateTags(): void { - const past = {}; + const past: {[key: string]: boolean} = {}; for (let index = 0; index < this.tags.length;) { const tag = this.tags[index]; if (past[tag]) { diff --git a/frontend/src/app/programs.component.html b/frontend/src/app/programs.component.html deleted file mode 100644 index 63a069a8..00000000 --- a/frontend/src/app/programs.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
- -

Add your own!

-
-
-
- -

{{program.name}}

-
-
-
diff --git a/frontend/src/app/programs.component.ts b/frontend/src/app/programs.component.ts deleted file mode 100644 index 8f0edfaa..00000000 --- a/frontend/src/app/programs.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component } from '@angular/core'; -import { OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { ProgramMetadata } from './program'; -// import { ProgramDetailComponent } from './program-detail.component'; -import { ProgramService } from './program.service'; - -@Component({ - selector: 'app-my-programs', - templateUrl: './programs.component.html', - styleUrls: ['./app.component.css'], - providers: [ProgramService] -}) - -export class ProgramsComponent implements OnInit { - programs: ProgramMetadata[]; - selectedProgram: ProgramMetadata; - - constructor( - private programService: ProgramService, - private router: Router - ) { - this.programService = programService; - this.router = router; - } - - getPrograms(): Promise { - return this.programService.getPrograms().then((programs) => this.programs = programs); - } - - gotoDetail(): void { - this.router.navigate(['/detail', this.selectedProgram.id]); - } - - ngOnInit(): void { - this.getPrograms(); - } - - gotoProgram(programId: number): void { - this.router.navigate(['/programs/' + programId]); - } - - onSelect(program: ProgramMetadata): void { - this.selectedProgram = program; - } - - addProgram(): void { - this.router.navigate(['/programs/add']); - } -} diff --git a/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.html b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.html new file mode 100644 index 00000000..50baeec8 --- /dev/null +++ b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.html @@ -0,0 +1,57 @@ +

Name your program and select it's type

+ + +
+ + Program name + + {{getNameErrorMessage()}} + + + +

Scratch's blocks

+
+ +
+
+

Classic block style, used by MIT's block-based editor.

+

Choose this if you already used MIT's Scratch.

+
+
+ + +

PrograMaker's Flow

+
+ +
+
+

PrograMaker's re-imagining of visual programming.

+

Choose this if you want to add visual elements to your program.

+
+
+
+ +
+ + + + diff --git a/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.scss b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.scss new file mode 100644 index 00000000..2c1ba58d --- /dev/null +++ b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.scss @@ -0,0 +1,57 @@ +.options-row { + margin: 1ex; + + &>mat-card { + // margin: 1ex; + } +} + +.options-row > mat-card { + &.selected { + background-color: rgba(170, 255, 170, 0.5); + } + + margin-left: 1ex; +} + +.example { + text-align: center; +} + +.example > img { + max-width: 100%; + max-height: 20ex; + padding: 1ex; + + background: #ddd; + border-radius: 5px; +} + +.description > .reason { + font-weight: bold; +} + +.description > .disabled-explanation { + background-color: #f66; + padding: 1ex; + border-radius: 5px; + color: black; + margin: 0.5ex; + text-align: center; + font-weight: bold; +} + + +.description > .warning { + background-color: #f66; + padding: 1ex; + border-radius: 5px; + color: black; + margin: 0.5ex; + text-align: center; + font-weight: bold; +} + +.title { + text-align: center; +} diff --git a/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.spec.ts b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.spec.ts new file mode 100644 index 00000000..e4e62358 --- /dev/null +++ b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.spec.ts @@ -0,0 +1,68 @@ +import { HttpClient } from '@angular/common/http'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { CookiesService } from '@ngx-utils/cookies'; +import { BrowserCookiesModule, BrowserCookiesService } from '@ngx-utils/cookies/browser'; +import { SelectProgrammingModelDialogComponent } from './select-programming-model-dialog.component'; + + +describe('SelectProgrammingModelDialogComponent', () => { + let component: SelectProgrammingModelDialogComponent; + let fixture: ComponentFixture; + + const mockDialogRef = { + close: jasmine.createSpy('close') + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SelectProgrammingModelDialogComponent ], + imports: [ + BrowserCookiesModule.forRoot(), + MatDialogModule, + NoopAnimationsModule, + ], + providers: [ + { + provide: CookiesService, + useClass: BrowserCookiesService, + }, + { + provide: HttpClient, + useValue: {} + }, + { + provide: MatDialogRef, + useValue: mockDialogRef + }, + { + provide: MAT_DIALOG_DATA, + useValue: [] // Add any data you wish to test if it is passed/used correctly + } + ] + }); + + + TestBed.overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [ SelectProgrammingModelDialogComponent ] + } + }); + + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectProgrammingModelDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + }); + + it('should create', () => { + expect(component).toBeTruthy(); + + }); +}); diff --git a/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.ts b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.ts new file mode 100644 index 00000000..b34cbe19 --- /dev/null +++ b/frontend/src/app/programs/select-programming-model-dialog/select-programming-model-dialog.component.ts @@ -0,0 +1,52 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ConnectionService } from '../../connection.service'; +import { ProgramType } from '../../program'; +import { ServiceService } from '../../service.service'; +import { SessionService } from '../../session.service'; + +@Component({ + selector: 'app-select-programming-model-dialog', + templateUrl: './select-programming-model-dialog.component.html', + styleUrls: ['./select-programming-model-dialog.component.scss'], + providers: [SessionService, ServiceService, ConnectionService], +}) +export class SelectProgrammingModelDialogComponent implements OnInit { + selectedType: ProgramType = null; + programName = new FormControl('', [Validators.required, Validators.minLength(4)]); + + constructor(public dialogRef: MatDialogRef, + public sessionService: SessionService, + public serviceService: ServiceService, + public connectionService: ConnectionService, + + @Inject(MAT_DIALOG_DATA) + public data: { is_advanced_user: boolean, is_user_in_preview: boolean }) { + this.selectedType = 'scratch_program'; + } + + ngOnInit(): void { + } + + + getNameErrorMessage() { + if (this.programName.hasError('required')) { + return 'You must enter a value'; + } + + return this.programName.hasError('minlength') ? 'Name must have at least 4 characters' : ''; + } + + selectType(program_type: ProgramType) { + this.selectedType = program_type; + } + + confirmSelection() { + this.dialogRef.close({success: true, program_type: this.selectedType, program_name: this.programName.value}); + } + + onNoClick(): void { + this.dialogRef.close({success: false, program_type: null, program_name: null}); + } +} diff --git a/frontend/src/app/resolvers/admin-stats.resolver.ts b/frontend/src/app/resolvers/admin-stats.resolver.ts new file mode 100644 index 00000000..9b36909d --- /dev/null +++ b/frontend/src/app/resolvers/admin-stats.resolver.ts @@ -0,0 +1,21 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { AdminService, PlatformStatsInfo } from "app/admin.service"; + + +@Injectable() +export class AdminStatsResolver implements Resolve { + constructor( + private adminService: AdminService, + ) {} + + resolve( + _route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + return this.adminService.getStats().catch(err => { + console.error(err); + return null; + }); + } +} diff --git a/frontend/src/app/resolvers/admin-user-list.resolver.ts b/frontend/src/app/resolvers/admin-user-list.resolver.ts new file mode 100644 index 00000000..295084c0 --- /dev/null +++ b/frontend/src/app/resolvers/admin-user-list.resolver.ts @@ -0,0 +1,21 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { AdminService, UserAdminData } from "app/admin.service"; + + +@Injectable() +export class AdminUserListResolver implements Resolve { + constructor( + private adminService: AdminService, + ) {} + + resolve( + _route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + return this.adminService.listAllUsers().catch(err => { + console.error(err); + return null; + }); + } +} diff --git a/frontend/src/app/resolvers/group-info-with-collaborators.resolver.ts b/frontend/src/app/resolvers/group-info-with-collaborators.resolver.ts new file mode 100644 index 00000000..22b053c8 --- /dev/null +++ b/frontend/src/app/resolvers/group-info-with-collaborators.resolver.ts @@ -0,0 +1,39 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { GroupInfo } from "app/group"; +import { GroupService } from "app/group.service"; +import { Collaborator } from "app/types/collaborator"; + +type Info = { info: GroupInfo, collaborators: Collaborator[] }; + +@Injectable() +export class GroupInfoWithCollaboratorsResolver implements Resolve { + constructor( + private groupService: GroupService, + ) {} + + async resolve( + route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + const params = route.params; + + const groupName = params.group_name; + + try { + const groupInfo = await this.groupService.getGroupWithName(groupName); + const collaborators = await this.groupService.getCollaboratorsOnGroup(groupInfo.id); + + const result = { + info: groupInfo, + collaborators: collaborators, + }; + + return result; + } + catch(err) { + console.error(err); + return null; + } + } +} diff --git a/frontend/src/app/resolvers/program-list.resolver.ts b/frontend/src/app/resolvers/program-list.resolver.ts new file mode 100644 index 00000000..af060eb9 --- /dev/null +++ b/frontend/src/app/resolvers/program-list.resolver.ts @@ -0,0 +1,47 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { GroupService } from "app/group.service"; +import { ProgramMetadata } from "app/program"; +import { ProgramService } from "app/program.service"; + + +@Injectable() +export class ProgramListResolver implements Resolve { + constructor( + private programService: ProgramService, + private groupService: GroupService, + ) {} + + async resolve( + route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + const params = route.params; + + let asGroup: string | null = null; + if (params.group_name) { + const groupName = params.group_name; + + try { + asGroup = (await this.groupService.getGroupWithName(groupName)).id; + } + catch(err) { + console.error(err); + return null; + } + } + + let programListing: Promise; + if (asGroup) { + programListing = this.programService.getProgramsOnGroup(asGroup); + } + else { + programListing = this.programService.getPrograms(); + } + + return programListing.catch(err => { + console.error(err); + return null; + }); + } +} diff --git a/frontend/src/app/resolvers/rendered-about.resolver.ts b/frontend/src/app/resolvers/rendered-about.resolver.ts new file mode 100644 index 00000000..9c18b751 --- /dev/null +++ b/frontend/src/app/resolvers/rendered-about.resolver.ts @@ -0,0 +1,32 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable, PLATFORM_ID, Inject } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { environment } from 'environments/environment'; +import { isPlatformServer } from "@angular/common"; + +@Injectable() +export class RenderedAboutResolver implements Resolve { + constructor( + private httpClient: HttpClient, + @Inject(PLATFORM_ID) private platformId: Object + ) {} + + resolve( + _route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + let url = environment.aboutPageRender; + + if (environment.SSRAboutPageRender && isPlatformServer(this.platformId)) { + // This cannot be rendered on server, so halt it's load + url = environment.SSRAboutPageRender; + } + + if (url) { + return this.httpClient.get(url, { responseType: 'text' as 'json' }).toPromise(); + } + else { + return Promise.resolve(null); + } + } +} diff --git a/frontend/src/app/resolvers/session.resolver.ts b/frontend/src/app/resolvers/session.resolver.ts new file mode 100644 index 00000000..343b3f48 --- /dev/null +++ b/frontend/src/app/resolvers/session.resolver.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { Session } from "app/session"; +import { SessionService } from "app/session.service"; + + +@Injectable() +export class SessionResolver implements Resolve { + constructor( + private sessionService: SessionService, + ) {} + + resolve( + _route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + return this.sessionService.getSession(); + } +} diff --git a/frontend/src/app/resolvers/user-bridges.resolver.ts b/frontend/src/app/resolvers/user-bridges.resolver.ts new file mode 100644 index 00000000..7e46c974 --- /dev/null +++ b/frontend/src/app/resolvers/user-bridges.resolver.ts @@ -0,0 +1,24 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { BridgeIndexData } from "app/bridges/bridge"; +import { BridgeService } from "app/bridges/bridge.service"; + +@Injectable() +export class UserBridgesResolver implements Resolve { + constructor( + private bridgeService: BridgeService, + ) {} + + async resolve( + _route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + try { + return (await this.bridgeService.listUserBridges()).bridges; + } + catch (err) { + console.error(err); + return null; + } + } +} diff --git a/frontend/src/app/resolvers/user-groups.resolver.ts b/frontend/src/app/resolvers/user-groups.resolver.ts new file mode 100644 index 00000000..27a35ee1 --- /dev/null +++ b/frontend/src/app/resolvers/user-groups.resolver.ts @@ -0,0 +1,21 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { GroupInfo } from "app/group"; +import { GroupService } from "app/group.service"; + +@Injectable() +export class UserGroupsResolver implements Resolve { + constructor( + private groupService: GroupService, + ) {} + + async resolve( + _route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + return this.groupService.getUserGroups().catch(err => { + console.error(err); + return null; + }); + } +} diff --git a/frontend/src/app/resolvers/user-profile.resolver.ts b/frontend/src/app/resolvers/user-profile.resolver.ts new file mode 100644 index 00000000..aa64a6f7 --- /dev/null +++ b/frontend/src/app/resolvers/user-profile.resolver.ts @@ -0,0 +1,30 @@ +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router"; +import { ProfileService, UserProfileInfo } from "app/profiles/profile.service"; +import { SessionService } from "app/session.service"; + +@Injectable() +export class UserProfileResolver implements Resolve { + constructor( + private profileService: ProfileService, + private sessionService: SessionService, + ) {} + + async resolve( + route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot + ): Promise { + const params = route.params; + + const userName = params.user_name ? params.user_name : (await this.sessionService.getSession()).username; + + if (!userName) { + return Promise.resolve(null); + } + + return this.profileService.getProfileFromUsername(userName).catch(err => { + console.error(err); + return null; + }); + } +} diff --git a/frontend/src/app/select_from_json.filter.ts b/frontend/src/app/select_from_json.filter.ts index 0cd7410e..72e4353d 100644 --- a/frontend/src/app/select_from_json.filter.ts +++ b/frontend/src/app/select_from_json.filter.ts @@ -14,7 +14,7 @@ export class SelectFromJSON implements PipeTransform { this.sanitizer = sanitizer; } - set_object_class(html, jtype) { + set_object_class(html: HTMLElement, jtype: JSONType) { switch (jtype) { case JSONType.Null: html.setAttribute('class', 'tagged-json-type-null'); @@ -44,7 +44,7 @@ export class SelectFromJSON implements PipeTransform { } - indent(element, depth) { + indent(element: HTMLElement, depth: number) { for (let i = 0; i < depth; i++) { const indent = document.createElement('span'); indent.setAttribute('class', 'tagged-json-indentation'); @@ -54,7 +54,7 @@ export class SelectFromJSON implements PipeTransform { } - tag(element, depth) { + tag(element: any, depth: number) { const jtype = GetTypeOfJson(element); const outer_object = document.createElement('span'); const json_object = document.createElement('span'); @@ -141,7 +141,7 @@ export class SelectFromJSON implements PipeTransform { } - transform(item): SafeHtml { + transform(item: any): SafeHtml { const tagged = this.tag(item, 1); tagged.setAttribute('key', ''); return this.sanitizer.bypassSecurityTrustHtml(tagged.outerHTML); diff --git a/frontend/src/app/service.service.ts b/frontend/src/app/service.service.ts index 27c65b74..49675c9b 100644 --- a/frontend/src/app/service.service.ts +++ b/frontend/src/app/service.service.ts @@ -1,21 +1,17 @@ - -import {map} from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { Service, AvailableService, ServiceEnableHowTo } from './service'; -import * as API from './api-config'; - - -import { SessionService } from './session.service'; import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; import { ContentType } from './content-type'; +import { AvailableService, ServiceEnableHowTo } from './service'; +import { SessionService } from './session.service'; +import { EnvironmentService } from './environment.service'; @Injectable() export class ServiceService { - private getServicesUrl = API.ApiRoot + '/services/'; - constructor( private http: HttpClient, private sessionService: SessionService, + private environmentService: EnvironmentService, ) { this.http = http; this.sessionService = sessionService; @@ -26,44 +22,139 @@ export class ServiceService { return userApiRoot + '/services/'; } + private getListAvailableServicesOnProgramUrl(programId: string): string { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/services`; + } + async getServiceEnableHowToUrl(service: AvailableService) { - return API.ApiHost + service.link + '/how-to-enable'; + return service.link + '/how-to-enable'; } - async getServiceRegistryUrl(service_id: string) { - const serviceRoot = await this.getListAvailableServicesUrl(); - return serviceRoot + 'id/' + service_id + '/register'; + getServiceEnableOnGroupHowToUrl(service: AvailableService, groupId: string) { + return `${this.environmentService.getApiRoot()}/services/by-id/${service.id}/how-to-enable`; + } + + getServiceEnableOnProgramHowToUrl(service: AvailableService, programId: string) { + return `${this.environmentService.getApiRoot()}/services/by-id/${service.id}/how-to-enable`; + } + + getServiceRegistryUrl(serviceId: string) { + return `${this.environmentService.getApiRoot()}/services/by-id/${serviceId}/register`; + } + getServiceRegistryUrlOnGroup(service_id: string) { + return `${this.environmentService.getApiRoot()}/services/by-id/${service_id}/register`; + } + + getServiceRegistryUrlOnProgram(bridgeId: string, programId: string) { + return `${this.environmentService.getApiRoot()}/programs/by-id/${programId}/services/by-id/${bridgeId}/register`; } getAvailableServices(): Promise { return this.getListAvailableServicesUrl().then( - url => this.http.get(url, { headers: this.sessionService.getAuthHeader() }).pipe( - map(response => response as AvailableService[])) + url => this.http.get(url, { headers: this.sessionService.getAuthHeader() }) + .pipe(map(response => response as AvailableService[])) .toPromise()); } - getHowToEnable(service: AvailableService): Promise { - return this.getServiceEnableHowToUrl(service).then( - url => this.http.get(url, { headers: this.sessionService.getAuthHeader() }).pipe( - map(response => { - return response as ServiceEnableHowTo; - })) - .toPromise()); + async getAvailableServicesOnProgram(programId: string): Promise { + const url = this.getListAvailableServicesOnProgramUrl(programId); + + return await this.http.get(url, { headers: this.sessionService.getAuthHeader() }) + .pipe(map(response => response as AvailableService[])) + .toPromise(); } - registerService(service_id: string, data: { [key: string]: string }): Promise { - return this.getServiceRegistryUrl(service_id).then( - url => this.http.post( - url, JSON.stringify(data), - { - headers: this.sessionService.addContentType( - this.sessionService.getAuthHeader(), - ContentType.Json), - }).pipe( - map(response => { - return (response as any).success; - })) - .toPromise()); + async getHowToEnable(service: AvailableService): Promise { + const url = await this.getServiceEnableHowToUrl(service); + return (this.http.get(url, { headers: this.sessionService.getAuthHeader() }) + .toPromise() as Promise); + } + + async getHowToEnableOnGroup(service: AvailableService, groupId: string): Promise { + const url = this.getServiceEnableOnGroupHowToUrl(service, groupId); + return (this.http.get(url, { headers: this.sessionService.getAuthHeader(), + params: { group_id: groupId } + }) + .toPromise() as Promise); + } + + getHowToEnableOnProgram(service: AvailableService, programId: string): Promise { + const url = this.getServiceEnableOnProgramHowToUrl(service, programId); + return (this.http.get(url, { headers: this.sessionService.getAuthHeader(), + params: { program_id: programId } + }) + .toPromise() as Promise); + } + + registerService(data: { [key: string]: string }, + service_id: string, + connection_id: string, + params?: { program_id: string, group_id: string } ): Promise<{success: boolean}> { + + if (connection_id) { + if (data.metadata === undefined) { + (data as any).metadata = {}; + } + (data as any).metadata.connection_id = connection_id; + } + + const cleanParams: { program_id?: string, group_id?: string } = {}; + if (params.program_id) { + cleanParams.program_id = params.program_id; + } + if (params.group_id) { + cleanParams.group_id = params.group_id; + } + + const url = this.getServiceRegistryUrl(service_id); + return (this.http.post( + url, JSON.stringify(data), + { + headers: this.sessionService.addContentType( + this.sessionService.getAuthHeader(), + ContentType.Json), + params: cleanParams, + }).toPromise()) as Promise<{success: boolean}>; + } + + async directRegisterService(bridge_id: string): Promise<{success: boolean}> { + const data = {}; + + const url = await this.getServiceRegistryUrl(bridge_id) + return (this.http.post( + url, JSON.stringify(data), + { + headers: this.sessionService.addContentType( + this.sessionService.getAuthHeader(), + ContentType.Json), + }).toPromise()) as Promise<{success: boolean}>; + } + + async directRegisterServiceOnGroup(bridgeId: string, groupId: string): Promise<{success: boolean}> { + const data = { }; + + const url = this.getServiceRegistryUrlOnGroup(bridgeId) + return (this.http.post( + url, JSON.stringify(data), + { + headers: this.sessionService.addContentType( + this.sessionService.getAuthHeader(), + ContentType.Json), + params: { group_id: groupId } + }).toPromise()) as Promise<{success: boolean}>; + } + + async directRegisterServiceOnProgram(bridgeId: string, programId: string): Promise<{ success: boolean; }> { + const data = { }; + + const url = this.getServiceRegistryUrlOnProgram(bridgeId, programId); + return (this.http.post( + url, JSON.stringify(data), + { + headers: this.sessionService.addContentType( + this.sessionService.getAuthHeader(), + ContentType.Json), + }).toPromise()) as Promise<{success: boolean}>; } } diff --git a/frontend/src/app/service.ts b/frontend/src/app/service.ts index e071fa6b..bf12b796 100644 --- a/frontend/src/app/service.ts +++ b/frontend/src/app/service.ts @@ -35,16 +35,30 @@ interface ServiceEnableTextEntry { type ServiceEnableEntry = ServiceEnableTextEntry | ServiceEnableTagEntry; type ServiceEnableType = 'message' | 'form'; +export interface TwoStepEnableMetadata { + service_id: string, + connection_id: string, +} + interface ServiceEnableMessage { type: ServiceEnableType; value: { form: ServiceEnableEntry[] }, - metadata?: any, + metadata?: TwoStepEnableMetadata, +} + +interface DirectServiceEnableMetadata { + service_id: string, +} + +type DirectEnable = { + type: 'direct', + metadata: DirectServiceEnableMetadata, } -type ServiceEnableHowTo = ServiceEnableMessage; +type ServiceEnableHowTo = ServiceEnableMessage | DirectEnable; export { Service, diff --git a/frontend/src/app/services/ui-signal.service.ts b/frontend/src/app/services/ui-signal.service.ts new file mode 100644 index 00000000..75717122 --- /dev/null +++ b/frontend/src/app/services/ui-signal.service.ts @@ -0,0 +1,180 @@ +import { Injectable, EventEmitter } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { SessionService } from 'app/session.service'; +import { EnvironmentService } from 'app/environment.service'; +import { toWebsocketUrl } from 'app/utils'; +import { Observable } from 'rxjs'; +import { startWith } from 'rxjs/operators'; + +type Message = any; + +@Injectable() +export class UiSignalService { + programId: string; + websocketEstablishment: Promise; + messageEmitter: EventEmitter; + private _gotInitialValues: boolean | 'in-progress' = false; + private _lastMessages: {[key: string]: any} = {}; + + constructor( + private http: HttpClient, + private sessionService: SessionService, + private environmentService: EnvironmentService, + ) { + this.http = http; + this.sessionService = sessionService; + this.messageEmitter = new EventEmitter(false); + } + + setProgramId(programId: string) { + if (programId !== this.programId) { + this._clearWebsocket(); + } + this.programId = programId; + } + + private _assertInitialized() { + if (!this.programId) { + throw Error("UiSignalService non initialized, ProgramId not set."); + } + } + + + _getInitialValuesUrl(): string { + return `${this.environmentService.getApiRoot()}/programs/by-id/${this.programId}/ui-values`; + } + + private _pullInitialValues() { + if (this._gotInitialValues) { + return; + } + this._gotInitialValues = 'in-progress'; + + const url = this._getInitialValuesUrl(); + (this.http.get(url, {headers: this.sessionService.getAuthHeader()}) + .toPromise() + .then((resp: any) => { + + const vals = resp.widget_values; + for (const widgetId of Object.keys(vals)) { + if (!this._lastMessages[widgetId]) { + + const message = { + key: 'ui_events_show', + subkey: widgetId, + values: vals[widgetId], + }; + this._lastMessages[widgetId] = message; + this.messageEmitter.emit(message); + } + } + + this._gotInitialValues = true; + })); + } + + _getWebsocketUrl(): string { + return toWebsocketUrl(this.environmentService, + `${this.environmentService.getApiRoot()}/programs/by-id/${this.programId}/ui-events`); + } + + private _clearWebsocket() { + if (this.websocketEstablishment) { + this.websocketEstablishment.then(ws => ws.close()); + } + this.websocketEstablishment = null; + } + + private _getWebsocket(): Promise { + if (!this.websocketEstablishment) { + this.websocketEstablishment = new Promise((resolve, reject) => { + const websocket = new WebSocket(this._getWebsocketUrl()); + + websocket.onopen = (() => { + const token = this.sessionService.getToken(); + + // Authenticate + websocket.send(JSON.stringify({ + "type": "AUTHENTICATION", + "value": {"token": token} + })); + + resolve(websocket); + }); + + websocket.onmessage = ((ev) => { + const parsed = JSON.parse(ev.data); + this.messageEmitter.emit(parsed); + }); + + websocket.onclose = (() => { + this._onWebscoketClose(); + }); + + websocket.onerror = ((ev) => { + this._onWebsocketError(ev); + reject(ev); + }); + + }); + + this.websocketEstablishment.catch(err => { + this._clearWebsocket(); + }) + } + + return this.websocketEstablishment; + } + + _onWebsocketError(ev: any) { + console.error("Websocket error", ev); + this._onWebscoketClose(); + } + + _onWebscoketClose() { + this._clearWebsocket(); + } + + public async sendBlockSignal(blockType: string, blockId: string): Promise { + this._assertInitialized(); + + const ws = await this._getWebsocket(); + ws.send(JSON.stringify({ + type: "ui-event", + value: { + action: "activated", + block_type: blockType, + block_id: blockId, + } + })); + } + + public onElementUpdate(blockType: string, blockId: string): Observable { + this._assertInitialized(); + this._getWebsocket(); // Create websocket if not existing + + if (!this._gotInitialValues) { + this._pullInitialValues(); + } + + const selector = `${blockType}.${blockId}`; + + const observer = new Observable(observer => { + this.messageEmitter.subscribe({ + next: (ev: any) => { + if (ev.subkey === selector) { + this._lastMessages[selector] = ev; + observer.next(ev); + } + }}); + }) + + if (this._lastMessages[selector]) { + return observer.pipe(startWith(this._lastMessages[selector])); + } + else { + return observer; + } + + } +} diff --git a/frontend/src/app/session.service.ts b/frontend/src/app/session.service.ts index 43ea8a1d..8d8cd4d7 100644 --- a/frontend/src/app/session.service.ts +++ b/frontend/src/app/session.service.ts @@ -1,73 +1,184 @@ - -import {map} from 'rxjs/operators'; -import { Injectable } from '@angular/core'; +import { isPlatformBrowser, isPlatformServer } from '@angular/common'; import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { CookiesService } from '@ngx-utils/cookies'; +import { Observable, Observer } from 'rxjs'; +import { BrowserService } from './browser.service'; +import { ContentType } from './content-type'; +import { EnvironmentService } from './environment.service'; import { Session } from './session'; +import * as progbar from './ui/progbar'; -import * as progbar from './ui/progbar'; -import * as API from './api-config'; -import { Observable } from 'rxjs'; -import { ApiRoot } from './api-config'; -import { ContentType } from './content-type'; +export type SessionInfoUpdate = { session: Session }; @Injectable() export class SessionService { - static EstablishedSession: Session = undefined; - checkSessionUrl = API.ApiRoot + '/sessions/check'; - loginUrl = API.ApiRoot + '/sessions/login'; - registerUrl = API.ApiRoot + '/sessions/register'; - registerValidateUrl = API.ApiRoot + '/sessions/register/verify'; - resetPasswordUrl = API.ApiRoot + '/sessions/login/reset'; - validatePasswordUpdateUrl = API.ApiRoot + '/sessions/login/reset/validate'; - passwordUpdateUrl = API.ApiRoot + '/sessions/login/reset/update'; + + private readonly INACTIVE_SESSION: Session = { + username: null, + user_id: null, + active: false, + tags: { + is_admin: false, + is_advanced: false, + is_in_preview: false, + } + }; + + private readonly COOKIE_TOKEN_KEY = 'programaker-auth'; + + // Keep track of the sessions and promises associated with each token. This + // is necessary when multiple users share the same SessionService, as might + // happen when doing ServerSideRender. + private readonly EstablishedSessions: {[key: string]: Session} = {}; + private readonly EstablishmentPromises: {[key: string]: Promise} = {}; + + // These static values help create a Singleton-like class. + // While not strtictly following the Singleton pattern, as class instances + // are obtained through Dependency Injection, it allows for every instance + // of the class to share the relevant state. + // + // The shared state are the Observable and Observer, and they are only + // available when runnin on browser to avoid session confusion. + private static sessionInfoObservable: Observable = null; + private static _sessionInfoObserver: Observer = null; constructor( private http: HttpClient, + private browser: BrowserService, + private cookies: CookiesService, + private environmentService: EnvironmentService, + @Inject(PLATFORM_ID) private platformId: Object, ) { this.http = http; } - storeToken(token: string) { - const storage = window.localStorage; - storage.setItem('session.service.token', token); + getCheckSessionUrl(): string { + return this.environmentService.getApiRoot() + '/sessions/check'; } - - removeToken() { - const storage = window.localStorage; - storage.removeItem('session.service.token'); + getLoginUrl(): string { + return this.environmentService.getApiRoot() + '/sessions/login'; } - - getToken(): string { - const storage = window.localStorage; - const token = storage.getItem('session.service.token'); - - return token; + getRegisterUrl(): string { + return this.environmentService.getApiRoot() + '/sessions/register'; } + getRegisterValidateUrl(): string { + return this.environmentService.getApiRoot() + '/sessions/register/verify'; + } + getResetPasswordUrl(): string { + return this.environmentService.getApiRoot() + '/sessions/login/reset'; + } + getValidatePasswordUpdateUrl(): string { + return this.environmentService.getApiRoot() + '/sessions/login/reset/validate'; + } + getPasswordUpdateUrl(): string { + return this.environmentService.getApiRoot() + '/sessions/login/reset/update'; + } + getUploadAssetToProgramIdUrl(programId: string): string { + return this.environmentService.getApiRoot() + `/programs/by-id/${programId}/assets`; + } + async getUserApiRoot(): Promise { - // tslint:disable-next-line:no-debugger - let session = SessionService.EstablishedSession; - if (session === undefined) { - session = await this.getSession(); + const session = await this.getSession(); + if (!session.active) { + throw Error("[getUserApiRoot] No active session"); } return this.getApiRootForUser(session.username); } getApiRootForUser(username: string): string { - return API.ApiRoot + '/users/' + username; + return this.environmentService.getApiRoot() + '/users/' + username; } async getApiRootForUserId(user_id?: string): Promise { if (!user_id) { - let session = SessionService.EstablishedSession; - if (session === undefined) { - session = await this.getSession(); + const session = await this.getSession(); + if (!session.active) { + throw Error("[getApiRootForUserId] No active session"); } user_id = session.user_id; } - return API.ApiRoot + '/users/id/' + user_id; + return this.environmentService.getApiRoot() + '/users/id/' + user_id; + } + + async getApiRootForUserIdNew(user_id?: string): Promise { + if (!user_id) { + const session = await this.getSession(); + if (!session.active) { + throw Error("[getApiRootForUserIdNew] No active session"); + } + user_id = session.user_id; + } + return this.environmentService.getApiRoot() + '/users/by-id/' + user_id; + } + + async getUserAvatarUrl(): Promise { + const root = await this.getApiRootForUserIdNew(); + + return `${root}/picture`; + } + + getUpdateUserAvatarUrl(): Promise { + return this.getUserAvatarUrl(); + } + + async generateApiTokenForScopes(scopes: string[]): Promise<{ token: string}> { + const url = this.environmentService.getApiRoot() + `/tokens`; + const req = await this.http.post(url, JSON.stringify({ scopes: scopes }), { + headers: this.addJsonContentType(this.getAuthHeader()), + }).toPromise(); + + + return (req as any).value; + } + + storeToken(token: string) { + const storage = this.browser.window.localStorage; + + storage.setItem('session.service.token', token); + this.cookies.put(this.COOKIE_TOKEN_KEY, token); + } + + removeToken() { + const storage = this.browser.window.localStorage; + storage.removeItem('session.service.token'); + this.cookies.remove(this.COOKIE_TOKEN_KEY); + } + + getToken(): string | null { + const storage = this.browser.window.localStorage; + + if (!storage) { // Might happen on SSR + const auth = this.cookies.get(this.COOKIE_TOKEN_KEY); + return auth || null; + } + + const token = storage.getItem('session.service.token'); + + return token; + } + + async updateUserSettings(user_settings_update : { is_advanced: boolean, is_in_preview?: boolean }): Promise { + const url = (await this.getApiRootForUserId()) + '/settings'; + const response = await (this.http + .post(url, JSON.stringify(user_settings_update), + { headers: this.addJsonContentType(this.getAuthHeader()) }) + .toPromise()); + + return (response as { success: boolean }).success; + } + + async updateUserProfileSettings(userProfile: { groups: string[] }) { + const url = `${await this.getApiRootForUserId()}/profile`; + const response = await (this.http + .post(url, JSON.stringify(userProfile), + { headers: this.addJsonContentType(this.getAuthHeader()) }) + .toPromise()); + + return (response as { success: boolean }).success; } getAuthHeader(): HttpHeaders { @@ -89,98 +200,144 @@ export class SessionService { return headers.append('content-type', contentType); } + async getSessionMonitor(): Promise<{session: Session, monitor: Observable | null}> { + if (isPlatformServer(this.platformId)) { + // This operation is used to change between sessions on browser. + // Trying to do that also on the server could (in theory) be + // done by tracking the token. + // + // As this is most interesting accross login operations, the token + // cannot be used (and should not happen during SSR, either), so the capability is removed on the server. + return { + session: await this.getSession(), + monitor: null, + }; + } + + if (!SessionService.sessionInfoObservable) { + this._monitorSession(); + } + + return { + session: await this.getSession(), + monitor: SessionService.sessionInfoObservable, + }; + } + getSession(): Promise { - if (this.getToken() === null) { - return Promise.resolve(null); + const token = this.getToken(); + if (token === null) { + // Return unactive session + return Promise.resolve({ + username: null, + user_id: null, + active: false, + tags: { + is_admin: false, + is_advanced: false, + is_in_preview: false, + } + }); + } + else { + // This is (hopefully) a temporary mechanism to migrate users that + // only have localStorage authentication to cookie-enabled auth. + this._duplicateSessionTokenToCookie(); + } + + if (this.EstablishedSessions[token] && this.EstablishedSessions[token].active) { + return Promise.resolve(this.EstablishedSessions[token]); } - return (this.http - .get(this.checkSessionUrl, { headers: this.getAuthHeader() }).pipe( - map((response) => { - const check = response as any; - const session = new Session(check.success, - check.username, - check.user_id); - SessionService.EstablishedSession = session; + if (!this.EstablishmentPromises[token]) { + const thisPromise = this.EstablishmentPromises[token] = this.forceUpdateSession().catch(err => { + console.error(err); - return session; - })) - .toPromise()); + // "Un-cache" the failed session + if (this.EstablishmentPromises[token] === thisPromise) { + this.EstablishmentPromises[token] = null; + this._updateSession(this.INACTIVE_SESSION); + } + + // Return unactive session + return Promise.resolve({ + username: null, + user_id: null, + active: false, + tags: { + is_admin: false, + is_advanced: false, + is_in_preview: false, + } + }); + }); + } + + return this.EstablishmentPromises[token]; } login(username: string, password: string): Promise { return progbar.track(this.http - .post(this.loginUrl, - JSON.stringify({ username: username, password: password }), - { headers: this.addJsonContentType(this.getAuthHeader()) }).pipe( - map(response => { - const data = response as any; - if (data.success) { - this.storeToken(data.token); - - const newSession = new Session(true, username, data.user_id); - SessionService.EstablishedSession = newSession; - - return true; - } - return false; - })) - .toPromise()); + .post(this.getLoginUrl(), + JSON.stringify({ username: username, password: password }), + { headers: this.addJsonContentType(this.getAuthHeader()) }).toPromise() + .then(async response => { + const data = response as any; + if (data.success) { + + this.storeToken(data.token); + await this.forceUpdateSession(); + + return true; + } + return false; + })); } logout() { + this._updateSession(this.INACTIVE_SESSION); this.removeToken(); - SessionService.EstablishedSession = undefined; } - register(username: string, email: string, password: string): Promise<{ success: boolean, continue_to_login: boolean}> { const headers = this.addJsonContentType(new HttpHeaders()); - return progbar.track(this.http - .post( - this.registerUrl, - JSON.stringify({ - username: username - , password: password - , email: email - }), - { headers }).pipe( - map(response => { - return { - success: (response as any).success, - continue_to_login: (response as any).ready - }; - })) - .toPromise()); + return progbar.track( + this.http + .post( + this.getRegisterUrl(), + JSON.stringify({ + username: username + , password: password + , email: email + }), + { headers }).toPromise() + .then(response => { + return { + success: (response as any).success, + continue_to_login: (response as any).ready + }})); } - validateRegisterCode(verificationCode: string): Promise { const headers = this.addJsonContentType(new HttpHeaders()); return progbar.track(this.http .post( - this.registerValidateUrl, + this.getRegisterValidateUrl(), JSON.stringify({ verification_code: verificationCode }), - { headers }).pipe( - map(response => { + { headers }).toPromise() + .then(response => { const success = (response as any).success; if (!success) { throw new Error(success.message); } - this.storeToken((response as any).session.token); - const session = new Session(true, - (response as any).session.username, - (response as any).session.user_id); - SessionService.EstablishedSession = session; - return session; - })) - .toPromise()); + return this.forceUpdateSession(); + })); } requestResetPassword(email: string): Promise { @@ -188,15 +345,14 @@ export class SessionService { return progbar.track(this.http .post( - this.resetPasswordUrl, + this.getResetPasswordUrl(), JSON.stringify({ email: email }), - { headers }).pipe( - map(_response => { + { headers }).toPromise() + .then(_response => { return; - })) - .toPromise()); + })); } validatePasswordUpdateCode(verificationCode: string): Promise { @@ -204,15 +360,14 @@ export class SessionService { return progbar.track(this.http .post( - this.validatePasswordUpdateUrl, + this.getValidatePasswordUpdateUrl(), JSON.stringify({ verification_code: verificationCode }), - { headers }).pipe( - map(_response => { + { headers }).toPromise() + .then(_response => { return; - })) - .toPromise()); + })); } resetPasswordUpdate(verificationCode: string, password: string): Promise { @@ -220,15 +375,83 @@ export class SessionService { return progbar.track(this.http .post( - this.passwordUpdateUrl, + this.getPasswordUpdateUrl(), JSON.stringify({ verification_code: verificationCode, password: password, }), - { headers }).pipe( - map(_response => { + { headers }).toPromise() + .then(_response => { return; - })) - .toPromise()); + })); + } + + async updateUserAvatar(image: File): Promise { + const formData = new FormData(); + formData.append('file', image); + + const url = await this.getUpdateUserAvatarUrl(); + + await this.http.post(url, formData, { headers: this.getAuthHeader() }).toPromise() + } + + async uploadAsset(file: File, programId: string): Promise<{ success: true, value: string }> { + const formData = new FormData(); + formData.append('file', file); + + const url = await this.getUploadAssetToProgramIdUrl(programId); + + const result = await this.http.post(url, formData, { headers: this.getAuthHeader() }).toPromise(); + + return result as any; + } + + private _monitorSession() { + SessionService.sessionInfoObservable = new Observable((observer) => { + SessionService._sessionInfoObserver = observer; + // This will be operated from `_updateSession` + }); + } + + private _updateSession(session: Session) { + const token = this.getToken(); + if (!token) { + if (session) { + throw new Error("Cannot update session with no token"); + } + else { + throw new Error("Finishing session when token is null, this should NOT happen"); + } + } + + this.EstablishedSessions[token] = session; + if (isPlatformBrowser(this.platformId) && SessionService._sessionInfoObserver) { + SessionService._sessionInfoObserver.next({ session: session }); + } + } + + private _duplicateSessionTokenToCookie() { + const token = this.getToken(); + if (token && !(this.cookies.get(this.COOKIE_TOKEN_KEY))) { + this.cookies.put(this.COOKIE_TOKEN_KEY, token); + } + } + + public async forceUpdateSession(): Promise { + if (this.getToken() === null) { + return Promise.resolve(null); + } + + const response = (await this.http + .get(this.getCheckSessionUrl(), { headers: this.getAuthHeader() }).toPromise()); + + const check = response as any; + const session = new Session(check.success, + check.username, + check.user_id, + check.tags); + this._updateSession(session); + + return session; } } diff --git a/frontend/src/app/session.ts b/frontend/src/app/session.ts index 1ebb6050..751374ab 100644 --- a/frontend/src/app/session.ts +++ b/frontend/src/app/session.ts @@ -1,11 +1,18 @@ -export class Session { +import {User, UserTags} from './user'; + +export class Session implements User { + // User interface username: string; - active: boolean; user_id: string; + tags: UserTags; + + // New elements + active: boolean; - constructor(active: boolean, username: string, user_id: string) { + constructor(active: boolean, username: string, user_id: string, tags: UserTags) { this.active = active; this.username = username; this.user_id = user_id; + this.tags = tags || { is_admin : false, is_advanced: false, is_in_preview: false }; } } diff --git a/frontend/src/app/settings/admin-settings/admin-settings.component.css b/frontend/src/app/settings/admin-settings/admin-settings.component.css new file mode 100644 index 00000000..a43d4c56 --- /dev/null +++ b/frontend/src/app/settings/admin-settings/admin-settings.component.css @@ -0,0 +1,35 @@ +.settings-section { + width: 100%; +} + +tbody tr:hover { + background-color: #eee; +} + +.warning-icon { + color: #ffa500; +} + +.error-icon { + color: #ff0000; +} + +.stat-card { + padding: 0.5ex; + border: 1px solid rgba(0,0,0,0.3); + text-align: center; +} + +.stat-card.running-service { + color: white; + background-color: #080; +} +.stat-card.stopped-service { + color: white; + background-color: #b00; +} + +.loading-message { + padding: 1ex; + text-align: center; +} diff --git a/frontend/src/app/settings/admin-settings/admin-settings.component.html b/frontend/src/app/settings/admin-settings/admin-settings.component.html new file mode 100644 index 00000000..34e5bb01 --- /dev/null +++ b/frontend/src/app/settings/admin-settings/admin-settings.component.html @@ -0,0 +1,61 @@ +
+
Loading stats...
+
+
+
{{service}}
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
UserIdUsernameEmailWarningsTagsReg. timeLast active
{{user.user_id.substring(0,9)}}… + {{user.username}} + + {{user.email}} + + {{note.icon}} + + + admin + advanced + in preview + + + {{getLocalRegistrationTime(user)}} + + {{getLocalLastActiveTime(user)}} +
+
+
diff --git a/frontend/src/app/settings/admin-settings/admin-settings.component.ts b/frontend/src/app/settings/admin-settings/admin-settings.component.ts new file mode 100644 index 00000000..34c05d68 --- /dev/null +++ b/frontend/src/app/settings/admin-settings/admin-settings.component.ts @@ -0,0 +1,140 @@ +import { Component, Inject, PLATFORM_ID } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Router, ActivatedRoute } from '@angular/router'; +import { AdminService, PlatformStatsInfo, UserAdminData } from 'app/admin.service'; +import { Session } from 'app/session'; +import { SessionService } from 'app/session.service'; +import { unixMsToStr } from 'app/utils'; +import { isPlatformBrowser } from '@angular/common'; + +interface Note { + icon: string; + text: string; +} + +const SYSTEM_STAT_RELOAD_TIME = 15000; // 15 seconds + +@Component({ + // moduleId: module.id, + selector: 'app-my-admin-settings', + templateUrl: './admin-settings.component.html', + providers: [AdminService, SessionService], + styleUrls: [ + 'admin-settings.component.css', + '../../libs/css/material-icons.css', + '../../libs/css/bootstrap.min.css', + ], +}) +export class AdminSettingsComponent { + session: Session; + users: UserAdminData[]; + notes: { [key: string]: Note[] } = {}; + stats: PlatformStatsInfo; + serviceNames: string[] = []; + + constructor( + public adminService: AdminService, + public sessionService: SessionService, + public router: Router, + private route: ActivatedRoute, + public dialog: MatDialog, + @Inject(PLATFORM_ID) private platformId: Object + ) { + this.route.data + .subscribe({ + next: (data: { session: Session, adminStats: PlatformStatsInfo, userList: UserAdminData[] }) => { + this.session = data.session; + if (!data.session.active) { + this.router.navigate(['/login'], {replaceUrl:true}); + return; + } + else if (!data.session.tags.is_admin) { + this.router.navigate(['/settings'], {replaceUrl:true}); + return; + } + + this.stats = data.adminStats; + this.serviceNames = Object.keys(this.stats.stats.active_services).sort(); + + for (const user of data.userList) { + this.annotate(user); + } + + this.users = data.userList.sort((x, y) => { + // Note that this sorting is reversed + if (x.registration_time > y.registration_time) { + return -1; + } + else if (y.registration_time > x.registration_time) { + return 1; + } + return 0; + } ); + }, + error: err => { + console.log('Error getting session', err); + this.router.navigate(['/login'], {replaceUrl:true}); + } + }); + + } + + // tslint:disable-next-line:use-life-cycle-interface + ngOnInit(): void { + // Don't enter an infinite loop when rendering on server + if (isPlatformBrowser(this.platformId)) { + setTimeout(this.reload_stats.bind(this), SYSTEM_STAT_RELOAD_TIME); + } + } + + reload_stats() { + this.adminService.getStats().then(stats => { + this.stats = stats; + this.serviceNames = Object.keys(stats.stats.active_services).sort(); + + // Don't enter an infinite loop when rendering on server + if (isPlatformBrowser(this.platformId)) { + setTimeout(this.reload_stats.bind(this), SYSTEM_STAT_RELOAD_TIME); + } + }).catch(err => { + console.error("Error loading stats:", err); + }); + } + + annotate(user: UserAdminData) { + // Check username + const notes: Note[] = []; + + if ((user.username.length < 4) || (user.username.length > 50)){ + notes.push({ + icon: "warning", // This is not allowed, but can be handled + text: "User name should have at least 4 and at most 50 characters", + }); + } + if (!user.username.match(/^[_a-zA-Z0-9]*$/)) { + notes.push({ + icon: "error", + text: "User name can only contain letters (a-z), digits (0-9) and underscores (_)", + }); + } + if (user.status !== 'ready') { + notes.push({ + icon: "warning", + text: "User status: " + user.status, + }); + } + + this.notes[user.user_id] = notes; + } + + getLocalRegistrationTime(user: UserAdminData): string { + return unixMsToStr(user.registration_time * 1000, { ms_precision: false }); + } + + getLocalLastActiveTime(user: UserAdminData): string { + if (!user.last_active_time) { + return 'none'; + } + return unixMsToStr(user.last_active_time * 1000, { ms_precision: false }); + } +} diff --git a/frontend/src/app/settings/group-settings/group-settings.component.css b/frontend/src/app/settings/group-settings/group-settings.component.css new file mode 100644 index 00000000..205c3ef1 --- /dev/null +++ b/frontend/src/app/settings/group-settings/group-settings.component.css @@ -0,0 +1,157 @@ +.settings-section { + width: 100%; + padding-bottom: 2em; +} + +.save-button, .delete-group-button { + padding: 1ex; + border-radius: 5px; + + /* Right align */ + margin: 1ex 0 auto auto; + display: block; +} + +.delete-group-button { + padding: 1ex; + border-radius: 5px; + margin: 0 auto; + margin-top: 2em; +} + +.settings-group { + background-color: #fff; + border-radius: 4px; + padding: 1ex; + margin-top: 1ex; + overflow: auto; + box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.3); +} + +.key-point { + text-decoration: underline; +} + +.settings-group > .title { + font-size: 1.5rem; + margin-bottom: 3ex; + border-bottom: 1px solid #aaa; +} + +.value:not(.code) { + float: right; +} + +.value.code { + margin-left: 1ex; + word-break: keep-all; +} + +.code { + padding: 1ex; + border-radius: 3px; + border: 1px solid #222; +} + +.toggle-setting { + margin-bottom: 5ex; +} + +.row { + margin: 0 !important; +} + +/* Avatar */ +div.avatar { + padding: 0; + height: 10em; + width: 10em; + display: block; + margin: 1em auto; + box-sizing: content-box; + border: 1px dashed transparent; +} + +.edited div.avatar { + border: 1px dashed rgba(0,0,0,0.3); +} + +.avatar > img { + height: 10em; + width: 10em; + border-radius: 4px; +} + +.avatar-edit-section { + text-align: center; +} + +.avatar-edit-section > mat-icon { + vertical-align: bottom; +} + +.picture-upload-button { + padding: 1ex; + border-radius: 5px; +} + +.avatar-edit-section .save-button { + display: inline-block; + margin-left: 1em; +} + +/* Collaborator list */ +mat-form-field.collaborator-input { + width: 100%; + margin-top: 1ex; +} + +ul.collaborator-list { + padding: 0; + max-width: 90vw; +} + +li.collaborator { + margin: 0.5ex; + font-size: small; + border-radius: 4px; +} + +button.remove-collaborator { + padding: 0; + margin: 0; + border-right: 1px solid rgba(0,0,0,0.5); + border: none; + border-radius: 4px; + min-height: 3ex; + vertical-align: sub; + background-color: #fa0; +} + +button.remove-collaborator:disabled { + color: transparent; + background-color: transparent; +} + +button.remove-collaborator mat-icon { + vertical-align: bottom; +} + +li.collaborator .name { + margin: 0 0.5ex 0 0.5ex; + font-weight: bold; +} + +mat-form-field.role { + margin-left: 1em; +} + +mat-select-trigger mat-icon { + font-size: 140%; + vertical-align: text-top; +} + +.collaborator-level-for-bridge-usage mat-select { + margin: 1rem 0 0 1rem; + width: calc(100% - 1rem); +} diff --git a/frontend/src/app/settings/group-settings/group-settings.component.html b/frontend/src/app/settings/group-settings/group-settings.component.html new file mode 100644 index 00000000..8d12c7be --- /dev/null +++ b/frontend/src/app/settings/group-settings/group-settings.component.html @@ -0,0 +1,100 @@ +
+
+
+
+ +
+
+ + + + +
+
+
+ +
+ + +
+
Administration
+
+
+ Which collaborators will be able to use the group's bridges for private programs? +
+ + + + {{ minCollaboratorForPrivateBridgeUsage !== 'not_allowed' + ? _roleToIcon(minCollaboratorForPrivateBridgeUsage) + : 'do_not_disturb' }} + + {{ group_admitted_for[minCollaboratorForPrivateBridgeUsage] }} + + + do_not_disturb {{ group_admitted_for['not_allowed'] }} + {{ _roleToIcon('admin') }} {{ group_admitted_for['admin'] }} + {{ _roleToIcon('editor') }} {{ group_admitted_for['editor'] }} + {{ _roleToIcon('viewer') }} {{ group_admitted_for['viewer'] }} + +
+ + +
+
+
Deletion
+
+ +
+
+
diff --git a/frontend/src/app/settings/group-settings/group-settings.component.ts b/frontend/src/app/settings/group-settings/group-settings.component.ts new file mode 100644 index 00000000..0fbaae22 --- /dev/null +++ b/frontend/src/app/settings/group-settings/group-settings.component.ts @@ -0,0 +1,167 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatButton } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BridgeService } from 'app/bridges/bridge.service'; +import { ConnectionService } from 'app/connection.service'; +import { GroupInfo } from 'app/group'; +import { GroupService, UserAutocompleteInfo } from 'app/group.service'; +import { MonitorService } from 'app/monitor.service'; +import { ProgramService } from 'app/program.service'; +import { ServiceService } from 'app/service.service'; +import { Session } from 'app/session'; +import { SessionService } from 'app/session.service'; +import { Collaborator, CollaboratorRole, roleToIcon } from 'app/types/collaborator'; +import { getGroupPictureUrl } from 'app/utils'; +import { ConfirmDeleteDialogComponent } from 'app/dialogs/confirm-delete-dialog/confirm-delete-dialog.component'; +import { GroupCollaboratorEditorComponent } from 'app/components/group-collaborator-editor/group-collaborator-editor.component'; +import { EnvironmentService } from 'app/environment.service'; + +const DEFAULT_ROLE : CollaboratorRole = 'editor'; + +@Component({ + // moduleId: module.id, + selector: 'app-my-group-settings', + templateUrl: './group-settings.component.html', + providers: [BridgeService, ConnectionService, GroupService, MonitorService, ProgramService, SessionService, ServiceService], + styleUrls: [ + 'group-settings.component.css', + '../../libs/css/material-icons.css', + '../../libs/css/bootstrap.min.css', + ], +}) +export class GroupSettingsComponent { + session: Session; + is_advanced: boolean; + groupInfo: GroupInfo; + + loadedImage: File = null; + minCollaboratorForPrivateBridgeUsage : CollaboratorRole | 'not_allowed'; + inServerMinCollabLevelForBridge : CollaboratorRole | 'not_allowed'; + + @ViewChild('imgPreview') imgPreview: ElementRef; + @ViewChild('imgFileInput') imgFileInput: ElementRef; + @ViewChild('saveAvatarButton') saveAvatarButton: MatButton; + + @ViewChild('saveCollaboratorsButton') saveCollaboratorsButton: MatButton; + @ViewChild('saveAdminSettingsButton') saveAdminSettingsButton: MatButton; + @ViewChild('saveAdminButton') saveAdminButton: MatButton; + @ViewChild('deletegroupButton') deletegroupButton: MatButton; + + collaborators: Collaborator[]; + @ViewChild('groupCollaboratorEditor') groupCollaboratorEditor: GroupCollaboratorEditorComponent; + + readonly _getGroupPicture: (userId: string) => string; + readonly _roleToIcon = roleToIcon; + + readonly group_admitted_for = { + not_allowed: 'Not allowed for anyone', + admin: 'Only Admins', + editor: 'Editors and Admins', + viewer: 'Viewers, Editors and Admins', + } + + constructor( + public sessionService: SessionService, + private route: ActivatedRoute, + public router: Router, + public dialog: MatDialog, + private groupService: GroupService, + private environmentService: EnvironmentService, + ) { + this._getGroupPicture = getGroupPictureUrl.bind(this, environmentService); + + this.route.data + .subscribe((data: { groupInfo: { info: GroupInfo, collaborators: Collaborator[] }, session: Session }) => { + this.session = data.session; + if (!data.session.active) { + this.router.navigate(['/login'], {replaceUrl:true}); + } + else { + this.is_advanced = this.session.tags.is_advanced; + this.groupInfo = data.groupInfo.info; + this.inServerMinCollabLevelForBridge = this.minCollaboratorForPrivateBridgeUsage = this.groupInfo.min_level_for_private_bridge_usage; + + this.collaborators = data.groupInfo.collaborators; + } + }); + } + + onChangeAdvancedSettings(event: MatSlideToggleChange) { + this.is_advanced = event.checked; + } + + async saveCollaborators() { + const buttonClass = this.saveCollaboratorsButton._elementRef.nativeElement.classList; + buttonClass.add('started'); + buttonClass.remove('completed'); + + await this.groupService.updateGroupCollaboratorList(this.groupInfo.id, this.groupCollaboratorEditor.getCollaborators()); + + buttonClass.remove('started'); + buttonClass.add('completed'); + } + + async saveAdminSettings() { + const buttonClass = this.saveAdminSettingsButton._elementRef.nativeElement.classList; + buttonClass.add('started'); + buttonClass.remove('completed'); + + await this.groupService.updateMinLevelForPrivateBridgeUsage(this.groupInfo.id, this.minCollaboratorForPrivateBridgeUsage); + + this.inServerMinCollabLevelForBridge = this.minCollaboratorForPrivateBridgeUsage; + + buttonClass.remove('started'); + buttonClass.add('completed'); + } + + 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.imgPreview.nativeElement.src = e.target.result as string; + } + + reader.readAsDataURL(input.files[0]); + } + } + + async saveAvatar() { + const buttonClass = this.saveAvatarButton._elementRef.nativeElement.classList; + buttonClass.add('started'); + buttonClass.remove('completed'); + + await this.groupService.updateGroupAvatar(this.groupInfo.id, this.loadedImage); + this.loadedImage = null; + + buttonClass.remove('started'); + buttonClass.add('completed'); + } + + startDeleteGroup() { + const programData = { name: this.groupInfo.name }; + + const dialogRef = this.dialog.open(ConfirmDeleteDialogComponent, { + data: programData + }); + + dialogRef.afterClosed().subscribe(result => { + if (!result) { + console.log("Cancelled"); + return; + } + + const deletion = (this.groupService.deleteGroup(this.groupInfo.id) + .then(() => this.router.navigateByUrl("/")) + .catch(() => { return false; })); + }); + } + +} diff --git a/frontend/src/app/settings/user-settings/settings.component.html b/frontend/src/app/settings/user-settings/settings.component.html new file mode 100644 index 00000000..ff59b639 --- /dev/null +++ b/frontend/src/app/settings/user-settings/settings.component.html @@ -0,0 +1,122 @@ +
+
+
+
+ +
+
+ + + + +
+
+
+ +
+ + +
+
Debug info
+ +
+ + {{session.user_id}} +
+
+ + +
+
diff --git a/frontend/src/app/settings/user-settings/settings.component.scss b/frontend/src/app/settings/user-settings/settings.component.scss new file mode 100644 index 00000000..3dc3559d --- /dev/null +++ b/frontend/src/app/settings/user-settings/settings.component.scss @@ -0,0 +1,138 @@ +.settings-section { + width: 100%; + padding-bottom: 2em; +} + +.save-button { + padding: 1ex; + border-radius: 5px; + + /* Right align */ + margin: 1ex 0 auto auto; + display: block; +} + +.settings-group { + background-color: #fff; + border-radius: 4px; + padding: 1ex; + margin-top: 1ex; + overflow: auto; + box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.3); + + & > .title { + font-size: 1.5rem; + margin-bottom: 1ex; + border-bottom: 1px solid #aaa; + } + + .settings-group { + box-shadow: none; + + & > .title::before { + content: '› '; + } + } +} + + +.toggle-setting .value { + float: left; + margin-left: 1em; +} + +.value.code { + margin-left: 1ex; + word-break: keep-all; +} + +.code { + display: inline-block; + padding: 1ex; + border-radius: 3px; + border: 1px solid #222; +} + +.toggle-setting { + margin-bottom: 5ex; +} + +.row { + margin: 0 !important; +} + +.annotated-icon.sample-button { + padding: 0.25ex; +} +.user-profile { + .section-title { + font-size: 150%; + margin: 1rem; + } + + .groups .group { + text-align: center; + margin: 0.2rem; + height: 4rem; + line-height: calc(4rem - 1rem); + padding: 0.5rem; + user-select: none; + } + + .groups .group .group-name { + } + .groups .group img { + max-width: 4rem; + max-height: 3rem; + } + .groups .group.unlisted { + background: repeating-linear-gradient( 45deg, + #fff, #fff 10px, + #eee 10px, #eee 20px ); + } + .groups .group.listed { + border: 1px solid rgba(175,0,175,0.5); + } + .groups .group.unlisted img { + filter: grayscale(100%); + } +} + +/* Avatar */ +div.avatar { + padding: 0; + height: 10em; + width: 10em; + display: block; + margin: 1em auto; + box-sizing: content-box; + border: 1px dashed transparent; +} + +.edited div.avatar { + border: 1px dashed rgba(0,0,0,0.3); +} + +.avatar > img { + height: 10em; + width: 10em; + border-radius: 4px; +} + +.avatar-edit-section { + text-align: center; +} + +.avatar-edit-section > mat-icon { + vertical-align: bottom; +} + +.picture-upload-button { + padding: 1ex; + border-radius: 5px; +} + +.avatar-edit-section .save-button { + display: inline-block; + margin-left: 1em; +} diff --git a/frontend/src/app/settings/user-settings/settings.component.ts b/frontend/src/app/settings/user-settings/settings.component.ts new file mode 100644 index 00000000..8f0fbf91 --- /dev/null +++ b/frontend/src/app/settings/user-settings/settings.component.ts @@ -0,0 +1,166 @@ +import * as progbar from 'app/ui/progbar'; + +import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; + +import { ProgramService } from 'app/program.service'; + +import { Session } from 'app/session'; +import { SessionService } from 'app/session.service'; + +import { ServiceService } from 'app/service.service'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; + +import { MonitorService } from 'app/monitor.service'; +import { ConnectionService } from 'app/connection.service'; +import { BridgeService } from 'app/bridges/bridge.service'; +import { MatButton } from '@angular/material/button'; +import { getUserPictureUrl, iconDataToUrl } from 'app/utils'; +import { EnvironmentService } from 'app/environment.service'; +import { ProgramMetadata } from 'app/program'; +import { GroupInfo } from 'app/group'; +import { BridgeIndexData } from 'app/bridges/bridge'; +import { UserProfileInfo } from 'app/profiles/profile.service'; + +@Component({ + // moduleId: module.id, + selector: 'app-my-settings', + templateUrl: './settings.component.html', + providers: [BridgeService, ConnectionService, MonitorService, ProgramService, SessionService, ServiceService], + styleUrls: [ + 'settings.component.scss', + '../../libs/css/material-icons.css', + '../../libs/css/bootstrap.min.css', + ], +}) +export class SettingsComponent { + session: Session; + is_advanced: boolean; + is_in_preview: boolean; + + loadedImage: File = null; + + @ViewChild('imgPreview') imgPreview: ElementRef; + @ViewChild('imgFileInput') imgFileInput: ElementRef; + @ViewChild('saveAvatarButton') saveAvatarButton: MatButton; + + readonly _getUserPicture: (userId: string) => string; + + groups: GroupInfo[]; + listedGroups: {[key: string]: boolean} = {}; + + bridges: BridgeIndexData[]; + + constructor( + public sessionService: SessionService, + public router: Router, + private route: ActivatedRoute, + private environmentService: EnvironmentService, + + public dialog: MatDialog, + ) { + this._getUserPicture = getUserPictureUrl.bind(this, environmentService); + + this.route.data + .subscribe((data: { groups: GroupInfo[], user_profile: UserProfileInfo }) => { + this.groups = data.groups; + + for (const group of data.user_profile.groups) { + this.listedGroups[group.id] = true; + } + }); + } + + // tslint:disable-next-line:use-life-cycle-interface + ngOnInit(): void { + this.route.data + .subscribe({ + next: (data: { session: Session }) => { + this.session = data.session; + if (!data.session.active) { + this.router.navigate(['/login'], {replaceUrl:true}); + } + else { + this.is_advanced = this.session.tags.is_advanced; + this.is_in_preview = this.session.tags.is_in_preview; + } + }, + error: err => { + console.log('Error getting session', err); + this.router.navigate(['/login'], {replaceUrl:true}); + } + }); + } + + onChangeAdvancedSettings(event: MatSlideToggleChange) { + this.is_advanced = event.checked; + } + + onChangeInPreviewSettings(event: MatSlideToggleChange) { + this.is_in_preview = event.checked; + } + + async saveUserSettings() { + // Send update + const button = document.getElementById('user-settings-save-button'); + if (button){ + button.classList.add('started'); + button.classList.remove('completed'); + } + + const publicGroups = []; + for (const group of Object.keys(this.listedGroups)) { + if (this.listedGroups[group]) { + publicGroups.push(group); + } + } + + await this.sessionService.updateUserProfileSettings({ groups: publicGroups }); + + const userSettings: {is_advanced: boolean, is_in_preview?: boolean} = { + is_advanced: this.is_advanced, + }; + + if (this.is_advanced) { + userSettings.is_in_preview = this.is_in_preview; + } + + const success = await this.sessionService.updateUserSettings(userSettings); + + if (success) { + this.session = await this.sessionService.forceUpdateSession(); + } + + if (button){ + button.classList.remove('started'); + button.classList.add('completed'); + } + } + + 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.imgPreview.nativeElement.src = e.target.result as string; + } + + reader.readAsDataURL(input.files[0]); + } + } + + async saveUserAvatar() { + const buttonClass = this.saveAvatarButton._elementRef.nativeElement.classList; + buttonClass.add('started'); + buttonClass.remove('completed'); + + await this.sessionService.updateUserAvatar(this.loadedImage); + + buttonClass.remove('started'); + buttonClass.add('completed'); + } +} diff --git a/frontend/src/app/summarize_json.filter.ts b/frontend/src/app/summarize_json.filter.ts index ae2c3d74..6b1d0e46 100644 --- a/frontend/src/app/summarize_json.filter.ts +++ b/frontend/src/app/summarize_json.filter.ts @@ -1,12 +1,13 @@ import { Pipe, PipeTransform } from '@angular/core'; import { GetTypeOfJson, JSONType } from './json'; +type JsonItem = string | JsonItem[] | {[key: string]: JsonItem}; + @Pipe({ name: 'SummarizeJSON', }) - export class SummarizeJSON implements PipeTransform { - transform(element) { + transform(element: any): JsonItem { const jtype = GetTypeOfJson(element); switch (jtype) { case JSONType.Null: @@ -28,7 +29,7 @@ export class SummarizeJSON implements PipeTransform { case JSONType.Map: { - const transformed = {}; + const transformed: {[key: string]: JsonItem} = {}; for (const key in element) { if (!element.hasOwnProperty(key)) { continue; diff --git a/frontend/src/app/syncronizer.ts b/frontend/src/app/syncronizer.ts new file mode 100644 index 00000000..5d4ca62e --- /dev/null +++ b/frontend/src/app/syncronizer.ts @@ -0,0 +1,4 @@ +import { Subscribable } from 'rxjs'; + +type Pusher = { push(value: T): void; }; +export type Synchronizer = Subscribable & Pusher; diff --git a/frontend/src/app/templates/create-dialog.component.ts b/frontend/src/app/templates/create-dialog.component.ts index f0ce0ece..703ae514 100644 --- a/frontend/src/app/templates/create-dialog.component.ts +++ b/frontend/src/app/templates/create-dialog.component.ts @@ -6,6 +6,7 @@ import { Template } from './template'; type VariableType = 'input' | 'ouput'; type PromiseHandler = { resolve: (value: [string, any[]]) => void, reject: Function }; +type TemplateChunk = { type: 'text' | 'line' | 'variable', content: string | TemplateChunk[], class?: string }; @Component({ selector: 'template-create-dialog-component', @@ -20,7 +21,7 @@ export class TemplateCreateDialogComponent { promise: PromiseHandler; selectedVar: HTMLElement; variables: string[]; - usedOutputs: {}; + usedOutputs: {[key: string]: boolean}; allOutputsUsed: boolean; constructor( @@ -224,7 +225,7 @@ export class TemplateCreateDialogComponent { return element; } - setOutputUsage(name, value) { + setOutputUsage(name: string, value: boolean) { this.usedOutputs[name] = value; this.checkAllOutputsUsed(); @@ -262,8 +263,8 @@ export class TemplateCreateDialogComponent { this.splitChildren(editor as HTMLElement, marker); } - extractTemplate(element: HTMLElement) { - const children = []; + extractTemplate(element: HTMLElement): TemplateChunk[] { + const children: TemplateChunk[] = []; for (let i = 0; i < element.childNodes.length; i++) { const node = element.childNodes[i]; diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/01-simple.spec.ts b/frontend/src/app/tests/graphic/ui-element-positioning/01-simple.spec.ts new file mode 100644 index 00000000..89468140 --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/01-simple.spec.ts @@ -0,0 +1,149 @@ +import { async, TestBed } from '@angular/core/testing'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { FlowWorkspace } from '../../../flow-editor/flow_workspace'; +import { SEPARATION as PositioningSeparation } from 'app/flow-editor/ui-blocks/renderers/positioning'; +import { configureTestBed } from './builder'; + +describe('FlowUI positioning: 01. Simple positioning. ', () => { + + beforeEach(async(() => { + configureTestBed(TestBed); + })); + + it('Should work vertically', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const graph: FlowGraph = { edges: [], nodes: { + // Page + page: { + data: { + value: { + "options": { + "type": "ui_flow_block", + "subtype": "container_flow_block", + "outputs": [], + "isPage": true, + "inputs": [], + "id": "responsive_page_holder", + }, + "extra": { + "dimensions": { + "width": 888.9020385742188, + "height": 856.9749755859375 + } + } + }, + type: "ui_flow_block", + subtype: "container_flow_block" + }, + container_id: null, + position: { x: 0, y: 0 }, + }, + + // Ui Horizontal Section + container: { + data: { + "value": { + "options": { + "type": "ui_flow_block", + "subtype": "container_flow_block", + "outputs": [], + "inputs": [], + "id": "horizontal_ui_section", + }, + "extra": { + "dimensions": { + "width": 886.9020385742188, + "height": 53.024993896484375 + } + } + }, + "type": "ui_flow_block", + "subtype": "container_flow_block" + }, + container_id: 'page', + position: { x: 10, y: 10 }, + }, + + // Simple element + el1: { + data: { + "value": { + "options": { + "type": "ui_flow_block", + "outputs": [ + { + "type": "pulse" + }, + { + "type": "string", + "name": "button text" + } + ], + "inputs": [], + "id": "simple_button", + }, + "extra": { + "textContent": "Element 1" + } + }, + "type": "ui_flow_block" + }, + container_id: 'container', + position: { x: 5, y: 20 }, + }, + + // Simple element + el2: { + data: { + "value": { + "options": { + "type": "ui_flow_block", + "outputs": [ + { + "type": "pulse" + }, + { + "type": "string", + "name": "button text" + } + ], + "inputs": [], + "id": "simple_button", + }, + "extra": { + "textContent": "Element 2" + } + }, + "type": "ui_flow_block" + }, + container_id: 'container', + position: { x: 7, y: 20 }, + }, + }}; + workspace.load(graph); + + workspace.repositionAll(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(4); + + // Sample assertions + expect(result.nodes.el1.position.x).toBeGreaterThan(result.nodes.container.position.x); + expect(result.nodes.el2.position.x).toBeGreaterThan(result.nodes.container.position.x); + + expect(result.nodes.el2.position.x).toBeGreaterThan(result.nodes.el1.position.x + PositioningSeparation); + + // Might be greater than because of the borders + expect(result.nodes.container.position.x).toBeGreaterThanOrEqual(result.nodes.page.position.x); + }); +}); diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/02-responsive-stability.spec.ts b/frontend/src/app/tests/graphic/ui-element-positioning/02-responsive-stability.spec.ts new file mode 100644 index 00000000..1bff7263 --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/02-responsive-stability.spec.ts @@ -0,0 +1,93 @@ +import { async, TestBed } from '@angular/core/testing'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { FlowWorkspace } from '../../../flow-editor/flow_workspace'; +import { SEPARATION } from 'app/flow-editor/ui-blocks/renderers/positioning'; +import { configureTestBed, pageGraph } from './builder'; +import { doesNotChangePositionsOnReposition } from './utils'; + +describe('FlowUI positioning: 02. Responsive stability.', () => { + + beforeEach(async(() => { + configureTestBed(TestBed); + })); + + it('Should work in the first invocation and be stable.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, [ topLeft, botLeft, right ]] = pageGraph([ + { + type: 'simple_button', + x: 10, y: 10, + }, + { + type: 'simple_button', + x: 10, y: 20 + }, + { + type: 'simple_button', + x: 100, y: 10, + } + ]); + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(4); + /** + * Source: + * +--------+ +--------+ + * | Btn1 | | Btn3 | + * +--------+ +--------+ + * +--------+ + * | Btn2 | + * +--------+ + * + * + * Expected result: + * +--------+ + * | Btn1 | + * +--------+ +--------+ + * | Btn3 | + * +--------+ +--------+ + * | Btn2 | + * +--------+ + */ + + const check = () => { + expect(result.nodes[topLeft].position.x + + workspace.getBlock(topLeft).getBodyArea().width + + SEPARATION).toEqual(result.nodes[right].position.x); + + expect(result.nodes[botLeft].position.x + + workspace.getBlock(topLeft).getBodyArea().width + + SEPARATION).toEqual(result.nodes[right].position.x); + + expect(result.nodes[topLeft].position.y + + workspace.getBlock(topLeft).getBodyArea().height + + SEPARATION).toEqual(result.nodes[botLeft].position.y); + + expect(result.nodes[topLeft].position.y).toBeLessThan(result.nodes[right].position.y); + expect(result.nodes[botLeft].position.y).toBeGreaterThan(result.nodes[right].position.y); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, [ topLeft, botLeft, right ]); + + // Check again + check(); + }); + +}); diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/03-bad-cases.spec.ts b/frontend/src/app/tests/graphic/ui-element-positioning/03-bad-cases.spec.ts new file mode 100644 index 00000000..95b831ae --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/03-bad-cases.spec.ts @@ -0,0 +1,94 @@ +import { async, TestBed } from '@angular/core/testing'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { FlowWorkspace } from '../../../flow-editor/flow_workspace'; +import { SEPARATION } from 'app/flow-editor/ui-blocks/renderers/positioning'; +import { configureTestBed, pageGraph } from './builder'; + +describe('FlowUI positioning: 03. Bad cases. ', () => { + + beforeEach(async(() => { + configureTestBed(TestBed); + })); + + it('Element right of horizontal separator.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, [ top, bot, sep, right ], page] = pageGraph([ + { + type: 'simple_button', + x: 10, y: 10, + }, + { + type: 'simple_button', + x: 10, y: 200 + }, + { + type: 'horizontal_separator', + x: 2, y: 100, + }, + { + type: 'simple_button', + x: 10, y: 100, + }, + ]); + + workspace.load(graph); + workspace.repositionAll(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(5); + /** + * Source: + * +--------+ + * | Btn1 | + * +--------+ + * +-----------------+ +--------+ + * | Horiz separator | | Btn3 | + * +-----------------+ +--------+ + * +--------+ + * | Btn2 | + * +--------+ + * + * + * Expected result: + * +--------+ + * | Btn1 | + * +--------+ + * +-----------------+ + * | Horiz separator | + * +-----------------+ + * +--------+ + * | Btn3 | + * +--------+ + * +--------+ + * | Btn2 | + * +--------+ + */ + + const check = () => { + for (const [up, down] of [[ top, sep ], [sep, right], [right, bot]]) { + expect(result.nodes[up].position.y + + workspace.getBlock(up).getBodyArea().height + + SEPARATION).toEqual(result.nodes[down].position.y); + } + }; + + // Check once + check(); + + // Re-position + workspace.repositionAll(); + + // Check again + check(); + }); + +}); diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/04-position-section-single-step.spec.ts b/frontend/src/app/tests/graphic/ui-element-positioning/04-position-section-single-step.spec.ts new file mode 100644 index 00000000..4b4a2c9b --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/04-position-section-single-step.spec.ts @@ -0,0 +1,644 @@ +import { async, TestBed } from '@angular/core/testing'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { FlowWorkspace } from '../../../flow-editor/flow_workspace'; +import { SEPARATION } from 'app/flow-editor/ui-blocks/renderers/positioning'; +import { configureTestBed, pageGraph } from './builder'; +import { doesNotChangePositionsOnReposition } from './utils'; +import { MAX_WIDTH as TEXT_MAX_WIDTH } from 'app/flow-editor/ui-blocks/renderers/fixed_text'; + +describe('FlowUI positioning: 04. Position sections in a single step.', () => { + + beforeEach(async(() => { + configureTestBed(TestBed); + })); + + it('3 elements in NON-nested section in heavily distorted positions.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + { + type: 'simple_button', + x: 10, y: 10, + }, + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + }, + ]); + + const [ topLevel, left_btn, center_btn, right_btn ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(5); + /** + * Layout: + * + * +------------------------------------+ + * | +-------+ +---------+ +--------+ | + * | | LeftB | | CenterB | | RightB | | + * | +-------+ +---------+ +--------+ | + * | Horizontal Section | + * +------------------------------------+ + * + **/ + + const check = () => { + // Button alignment + for (const [pair, pair_left, pair_right] of [['left-center', left_btn, center_btn], ['center-right', center_btn, right_btn]]) { + const areaLeft = workspace.getBlock(pair_left).getBodyArea(); + const areaRight = workspace.getBlock(pair_right).getBodyArea(); + + expect(areaLeft.y).toBe(areaRight.y); + + expect(areaLeft.x + areaLeft.width) + .toBe(areaRight.x - SEPARATION, + `On button pair ${pair}. ${areaLeft.x} + ${areaLeft.width} =/= ${areaRight.x} - ${SEPARATION}`); + } + + const areaTopLevel = workspace.getBlock(topLevel).getBodyArea(); + // Button position on section + for (const [name, btn] of [['left', left_btn], ['center', center_btn], ['right', right_btn]]) { + const areaButton = workspace.getBlock(btn).getBodyArea(); + + expect(areaTopLevel.height) + .toBe(areaButton.height + SEPARATION * 2, + `On section-group ${name}. Height=${areaTopLevel.height} =/= ${areaButton.height} + ${SEPARATION} * 2 `); + + expect(areaTopLevel.y + SEPARATION) + .toBe(areaButton.y, + `On section-group ${name}. Y=${areaTopLevel.y} + ${SEPARATION} =/= ${areaButton.y}`); + } + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + it('3 elements in NESTED section in heavily distorted positions.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + }, + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + }, + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + } + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + }, + ]); + + const [ topLevel, left, left_btn, center, center_btn, right, right_btn ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(8); + /** + * Layout: + * + * +------------------------------------+ + * |+---------++-----------++----------+| + * ||+-------+||+---------+||+--------+|| + * ||| LeftB |||| CenterB |||| RightB ||| + * ||+-------+||+---------+||+--------+|| + * || Vert || Vert || Vert || + * || Section || Section || Section || + * |+---------++-----------++----------+| + * | Horizontal Section | + * +------------------------------------+ + * + **/ + + const check = () => { + // Button alignment + for (const [pair, pair_left, pair_right] of [['left-center', left_btn, center_btn], ['center-right', center_btn, right_btn]]) { + const areaLeft = workspace.getBlock(pair_left).getBodyArea(); + const areaRight = workspace.getBlock(pair_right).getBodyArea(); + + expect(areaLeft.y).toBe(areaRight.y); + + expect(areaLeft.x + areaLeft.width + SEPARATION * 2) + .toBe(areaRight.x - SEPARATION, + `On button pair ${pair}. ${areaLeft.x} + ${areaLeft.width} + ${SEPARATION * 2} =/= ${areaRight.x} - ${SEPARATION}`); + } + + // Section alignment + for (const [pair, pair_left, pair_right] of [['left-center', left, center], ['center-right', center, right]]) { + const areaLeft = workspace.getBlock(pair_left).getBodyArea(); + const areaRight = workspace.getBlock(pair_right).getBodyArea(); + + expect(areaLeft.y).toBe(areaRight.y); + expect(areaLeft.x + areaLeft.width) + .toBe(areaRight.x - SEPARATION, + `On section pair ${pair}. ${areaLeft.x} + ${areaLeft.width} =/= ${areaRight.x} - ${SEPARATION}`); + } + + // Button position on section + for (const [name, section, btn] of [['left', left, left_btn], ['center', center, center_btn], ['right', right, right_btn]]) { + const areaSection = workspace.getBlock(section).getBodyArea(); + const areaButton = workspace.getBlock(btn).getBodyArea(); + + expect(areaSection.width) + .toBe(areaButton.width + SEPARATION * 2, + `On section-group ${name}. Width=${areaSection.width} =/= ${areaButton.width} + ${SEPARATION} * 2 `); + + expect(areaSection.x + SEPARATION) + .toBe(areaButton.x, + `On section-group ${name}. X=${areaSection.x} + ${SEPARATION} =/= ${areaButton.x}`); + + expect(areaSection.height) + .toBe(areaButton.height + SEPARATION * 2, + `On section-group ${name}. Height=${areaSection.height} =/= ${areaButton.height} + ${SEPARATION} * 2 `); + + expect(areaSection.y + SEPARATION) + .toBe(areaButton.y, + `On section-group ${name}. Y=${areaSection.y} + ${SEPARATION} =/= ${areaButton.y}`); + } + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + it('3 elements in a SINGLE NESTED section in heavily distorted positions.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + { + type: 'simple_button', + x: 10, y: 11, + }, + { + type: 'simple_button', + x: 10, y: 12, + }, + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + } + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + }, + ]); + + const [ topLevel, vert, top_btn, center_btn, bot_btn ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(6); + /** + * Layout: + * + * +-------------+ + * |+-----------+| + * ||+---------+|| + * ||| Top Btn ||| + * ||+---------+|| + * ||+---------+|| + * ||| CenterB ||| + * ||+---------+|| + * ||+---------+|| + * ||| BottomB ||| + * ||+---------+|| + * || Vertical || + * || Section || + * |+-----------+| + * | Horizontal | + * | Section | + * +-------------+ + * + **/ + + const check = () => { + // Button alignment + for (const [pair, pair_top, pair_bottom] of [['top-center', top_btn, center_btn], ['center-bot', center_btn, bot_btn]]) { + const areaTop = workspace.getBlock(pair_top).getBodyArea(); + const areaBottom = workspace.getBlock(pair_bottom).getBodyArea(); + + expect(areaTop.x).toBe(areaBottom.x); + + expect(areaTop.y + areaTop.height + SEPARATION) + .toBe(areaBottom.y, + `On button pair ${pair}. ${areaTop.y} + ${areaTop.height} + ${SEPARATION} =/= ${areaBottom.y}`); + } + + const areaTopLevel = workspace.getBlock(topLevel).getBodyArea(); + // Button position on section + for (const [name, btn] of [['top', top_btn], ['center', center_btn], ['bottom', bot_btn]]) { + const areaButton = workspace.getBlock(btn).getBodyArea(); + + expect(areaTopLevel.width) + .toBe(areaButton.width + SEPARATION * 4, + `On section-group ${name}. Width=${areaTopLevel.width} =/= ${areaButton.width} + ${SEPARATION} * 4`); + + expect(areaTopLevel.x + SEPARATION * 2) + .toBe(areaButton.x, + `On section-group ${name}. X=${areaTopLevel.x} + ${SEPARATION} * 2 =/= ${areaButton.x}`); + } + + const areaTopButton = workspace.getBlock(top_btn).getBodyArea(); + const areaBotButton = workspace.getBlock(bot_btn).getBodyArea(); + + expect(areaTopLevel.y + SEPARATION) + .toBe(areaTopButton.y, + `Y=${areaTopLevel.y} + ${SEPARATION} =/= ${areaTopButton.y}`); + + expect(areaTopLevel.y + areaTopLevel.height) + .toBe(areaBotButton.y + areaBotButton.height + SEPARATION, + `Y=${areaTopLevel.y} + ${areaTopLevel.height} =/= ${areaBotButton.y} + ${areaBotButton.height} + ${SEPARATION}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + it('3 elements in NESTED HORIZONTALS in a VERTICAL SINGLE NESTED section in heavily distorted positions.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ] + }, + { + type: "horizontal_ui_section", + x: 10, y: 101, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ] + }, + { + type: "horizontal_ui_section", + x: 10, y: 102, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ] + }, + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + } + ], + extra: { dimensions: { width: 9999, height: 9999 }}, + }, + ]); + + const [ topLevel, vert, top, top_btn, center, center_btn, bot, bot_btn ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(9); + /** + * Layout: + * + * +--------------------+ + * | | + * | +---------------+ | + * | | +-----------+ | | + * | | |+---------+| | | + * | | || Top Btn || | | + * | | |+---------+| | | + * | | |Horizontal | | | + * | | | Section | | | + * | | +-----------+ | | + * | | | | + * | | +-----------+ | | + * | | |+---------+| | | + * | | || CenterB || | | + * | | |+---------+| | | + * | | |Horizontal | | | + * | | | Section | | | + * | | +-----------+ | | + * | | | | + * | | +-----------+ | | + * | | |+---------+| | | + * | | || BottomB || | | + * | | |+---------+| | | + * | | |Horizontal | | | + * | | | Section | | | + * | | +-----------+ | | + * | | Vertical | | + * | | Section | | + * | +---------------+ | + * | | + * | Horizontal | + * | Section | + * +--------------------+ + **/ + + const check = () => { + // Button alignment + for (const [pair, pair_top, pair_bottom] of [['top-center', top_btn, center_btn], ['center-bot', center_btn, bot_btn]]) { + const areaTop = workspace.getBlock(pair_top).getBodyArea(); + const areaBottom = workspace.getBlock(pair_bottom).getBodyArea(); + + expect(areaTop.x).toBe(areaBottom.x); + + expect(areaTop.y + areaTop.height + SEPARATION * 2) + .toBe(areaBottom.y - SEPARATION, + `On button pair ${pair}. ${areaTop.y} + ${areaTop.height} + ${SEPARATION} =/= ${areaBottom.y} - ${SEPARATION}`); + } + + // Section alignment + for (const [pair, pair_top, pair_bottom] of [['top-center', top, center], ['center-bottom', center, bot]]) { + const areaTop = workspace.getBlock(pair_top).getBodyArea(); + const areaBottom = workspace.getBlock(pair_bottom).getBodyArea(); + + expect(areaTop.x).toBe(areaBottom.x); + expect(areaTop.y + areaTop.height) + .toBe(areaBottom.y - SEPARATION, + `On section pair ${pair}. ${areaTop.y} + ${areaTop.height} =/= ${areaBottom.y} - ${SEPARATION}`); + } + + // Button position on section + for (const [name, section, btn] of [['top', top, top_btn], ['center', center, center_btn], ['bottom', bot, bot_btn]]) { + const areaSection = workspace.getBlock(section).getBodyArea(); + const areaButton = workspace.getBlock(btn).getBodyArea(); + + expect(areaSection.width) + .toBe(areaButton.width + SEPARATION * 2, + `On section-group ${name}. Width=${areaSection.width} =/= ${areaButton.width} + ${SEPARATION} * 2`); + + expect(areaSection.x + SEPARATION) + .toBe(areaButton.x, + `On section-group ${name}. X=${areaSection.x} + ${SEPARATION} =/= ${areaButton.x}`); + + expect(areaSection.height) + .toBe(areaButton.height + SEPARATION * 2, + `On section-group ${name}. Height=${areaSection.height} =/= ${areaButton.height} + ${SEPARATION} * 2 `); + + expect(areaSection.y + SEPARATION) + .toBe(areaButton.y, + `On section-group ${name}. Y=${areaSection.y} + ${SEPARATION} =/= ${areaButton.y}`); + } + + const areaTopLevel = workspace.getBlock(topLevel).getBodyArea(); + const areaTopButton = workspace.getBlock(top_btn).getBodyArea(); + const areaBotButton = workspace.getBlock(bot_btn).getBodyArea(); + + expect(areaTopLevel.y + SEPARATION * 2) + .toBe(areaTopButton.y, + `On section-group ${name}. Y=${areaTopLevel.y} + ${SEPARATION} * 2 =/= ${areaTopButton.y}`); + + expect(areaTopLevel.y + areaTopLevel.height) + .toBe(areaBotButton.y + areaBotButton.height + SEPARATION * 2, + `Y=${areaTopLevel.y} + ${areaTopLevel.height} =/= ${areaBotButton.y} + ${areaBotButton.height} + ${SEPARATION} * 2`); + + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + + it('Long and short FixedText separated by orizontalSeparator.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: 'fixed_text', + x: 10, y: 10, + extra: { + content: [ { + value: 'A small text', + type: 'text' + }], + }, + }, + { + type: 'horizontal_separator', + x: 10, y: 12, + extra: { + dimensions: { width: TEXT_MAX_WIDTH * 2, height: 100 }, + } + }, + { + type: 'fixed_text', + x: 10, y: 14, + extra: { + content: [ { + value: 'A long text, which will take more than MAX_WIDTH and will require looking for the proper text break point so it fits on the allocated size, even if it is in a single line.', + type: 'text' + }], + }, + }, + ]); + + const [ textTop, separator, textBot ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(blocks.length + 1); + + /** + * Layout: + * + * +---------+ + * | Text1 | + * +---------+ + * +----------------+ + * | Separator1 | + * +----------------+ + * +------------+ + * | Long Text2 | + * +------------+ + * + **/ + + const check = () => { + const topTextArea = workspace.getBlock(textTop).getBodyArea(); + const bottomTextArea = workspace.getBlock(textBot).getBodyArea(); + const separatorArea = workspace.getBlock(separator).getBodyArea(); + const pageArea = workspace.getBlock(page).getBodyArea(); + + // Separator has to take all width + expect(separatorArea.x).toEqual(pageArea.x); + expect(separatorArea.width).toEqual(pageArea.width); + + // Top text (small) has to be centered + expect(topTextArea.x + topTextArea.width / 2 ) + .toBe(pageArea.x + pageArea.width / 2, + `TopText centered in page. ${topTextArea.x} + ${topTextArea.width} / 2 =/= ${pageArea.x} + ${pageArea.width} / 2`); + + // Bottom text (large) has to take all width (minus separation) + expect(bottomTextArea.x) + .toBe(pageArea.x + SEPARATION, + `Bottom text must start just after SEPARATION. ${bottomTextArea.x} =/= ${pageArea.x} + ${SEPARATION}`); + expect(bottomTextArea.width + SEPARATION * 2) + .toBe(pageArea.width, + `Bottom text must take all width minus the SEPARATION left and right. ${bottomTextArea.width} + ${SEPARATION} * 2 =/= ${pageArea.width}`); + + + // Vertically, there must be a separation between the elements + expect(topTextArea.y + topTextArea.height + SEPARATION) + .toBe(separatorArea.y, + `TopText → Separator. ${topTextArea.y} + ${topTextArea.height} + ${SEPARATION} =/= ${separatorArea.y}`); + + expect(separatorArea.y + separatorArea.height + SEPARATION) + .toBe(bottomTextArea.y, + `Separator → BottomText. ${separatorArea.y} + ${separatorArea.height} + ${SEPARATION} =/= ${bottomTextArea.y}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); +}); diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/05-image-positioning.spec.ts b/frontend/src/app/tests/graphic/ui-element-positioning/05-image-positioning.spec.ts new file mode 100644 index 00000000..e0a0a485 --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/05-image-positioning.spec.ts @@ -0,0 +1,118 @@ +import { async, TestBed } from '@angular/core/testing'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { FlowWorkspace } from '../../../flow-editor/flow_workspace'; +import { SEPARATION } from 'app/flow-editor/ui-blocks/renderers/positioning'; +import { configureTestBed, pageGraph } from './builder'; +import { doesNotChangePositionsOnReposition } from './utils'; +import { MIN_WIDTH as MIN_PAGE_WIDTH } from '../../../flow-editor/ui-blocks/renderers/responsive_page'; + +describe('FlowUI positioning: 05. Image positioning.', () => { + + beforeEach(async(() => { + configureTestBed(TestBed); + })); + + it('IMAGE positioning with other elements in OTHER section.', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: "horizontal_ui_section", + x: 10, y: 100, + contents: [ + { + type: 'fixed_image', + x: 10, y: 10, + extra: { dimensions: { width: MIN_PAGE_WIDTH + 100, height: MIN_PAGE_WIDTH + 100 }}, + }, + ], + extra: { dimensions: { width: 500, height: 500 }}, + }, + { + type: "horizontal_ui_section", + x: 10, y: 101, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + extra: { dimensions: { width: 500, height: 500 }}, + }, + ]); + + const [ top, top_img, bot, bot_btn ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(5); + /** + * Layout: + * + * +------------+ + * | | + * | +-------+ | + * | | Image | | + * | +-------+ | + * | Horizontal | + * | Section | + * +------------+ + * +------------+ + * | | + * | +--------+ | + * | | Button | | + * | +--------+ | + * | Horizontal | + * | Section | + * +------------+ + * + **/ + + const check = () => { + const pageArea = workspace.getBlock(page).getBodyArea(); + + for (const [name, section, element] of [['top', top, top_img], ['bottom', bot, bot_btn]]) { + // Element position on section + const areaSection = workspace.getBlock(section).getBodyArea(); + const areaElement = workspace.getBlock(element).getBodyArea(); + + expect(areaSection.height) + .toBe(areaElement.height + SEPARATION * 2, + `On section-group ${name}. Height=${areaSection.height} =/= ${areaElement.height} + ${SEPARATION} * 2 `); + + expect(areaSection.y + SEPARATION) + .toBe(areaElement.y, + `On section-group ${name}. Y=${areaSection.y} + ${SEPARATION} =/= ${areaElement.y}`); + + // Everything inside the page + expect(areaSection.x) + .toBe(pageArea.x, + `On section-group ${name}. X=${areaSection.x} =/= ${pageArea.x}`); + + expect(areaElement.x) + .toBeGreaterThanOrEqual(pageArea.x, + `On section-group ${name}. X=${areaElement.x} < ${pageArea.x}`); + } + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); +}); diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/06-position-responsive-container-single-step.spec.ts b/frontend/src/app/tests/graphic/ui-element-positioning/06-position-responsive-container-single-step.spec.ts new file mode 100644 index 00000000..8485e92c --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/06-position-responsive-container-single-step.spec.ts @@ -0,0 +1,411 @@ +import { async, TestBed } from '@angular/core/testing'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { FlowWorkspace } from '../../../flow-editor/flow_workspace'; +import { SEPARATION } from 'app/flow-editor/ui-blocks/renderers/positioning'; +import { configureTestBed, pageGraph } from './builder'; +import { doesNotChangePositionsOnReposition } from './utils'; + +describe('FlowUI positioning: 06. Position responsive container.', () => { + + beforeEach(async(() => { + configureTestBed(TestBed); + })); + + // ---------------------------- + // UI Card tests + // ---------------------------- + it('Simple UI cards should have a stable position', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: "simple_card", + x: 10, y: 10, + contents: [ + { + type: 'horizontal_ui_section', + x: 10, y: 10, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + }, + ], + }, + ]); + + const [ card, section, button ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(blocks.length + 1); + /** + * Layout: + * +--------------+ + * |+------------+| + * || +--------+ || + * || | Button | || + * || +--------+ || + * || Horizontal || + * || Section || + * |+------------+| + * | UI Card | + * +--------------+ + * + **/ + + + const check = () => { + const cardArea = workspace.getBlock(card).getBodyArea(); + const sectionArea = workspace.getBlock(section).getBodyArea(); + const buttonArea = workspace.getBlock(button).getBodyArea(); + const pageArea = workspace.getBlock(page).getBodyArea(); + + // Card has to has to take all width (minus separation) + expect(cardArea.x) + .toBe(pageArea.x + SEPARATION, + `${cardArea.x} =/= ${pageArea.x} + ${SEPARATION}`); + expect(cardArea.width + SEPARATION * 2) + .toBe(pageArea.width, + `${cardArea.width} + ${SEPARATION} * 2 =/= ${pageArea.width}`); + + // Section has to take all width on card, and all height minus separation + expect(sectionArea.x).toEqual(cardArea.x); + expect(sectionArea.width).toEqual(cardArea.width); + + expect(sectionArea.y) + .toBe(cardArea.y + SEPARATION, + `${sectionArea.y} =/= ${cardArea.y} + ${SEPARATION}`); + expect(sectionArea.height + SEPARATION * 2) + .toBe(cardArea.height, + `${sectionArea.height} + ${SEPARATION} * 2 =/= ${cardArea.height}`); + + // Button must be SEPARATION to the left & top of the section + expect(sectionArea.y + SEPARATION) + .toBe(buttonArea.y, + `${sectionArea.y} + ${SEPARATION} =/= ${buttonArea.y}`); + + expect(sectionArea.x + SEPARATION) + .toBe(buttonArea.x, + `${sectionArea.x} + ${SEPARATION} =/= ${buttonArea.x}`); + + expect(buttonArea.y + buttonArea.height + SEPARATION) + .toBe(sectionArea.y + sectionArea.height, + `${buttonArea.y} + ${buttonArea.height} + ${SEPARATION} =/= ${sectionArea.y} + ${sectionArea.height}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + it('Simple UI cards should have a stable position even out of Pages', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, _page] = pageGraph([ + { + type: "simple_card", + x: 10, y: 10, + contents: [ + { + type: 'horizontal_ui_section', + x: 10, y: 10, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + }, + ], + }, + ], { noPage: true }); + + const [ card, section, button ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(blocks.length); + /** + * Layout: + * +--------------+ + * |+------------+| + * || +--------+ || + * || | Button | || + * || +--------+ || + * || Horizontal || + * || Section || + * |+------------+| + * | UI Card | + * +--------------+ + * + **/ + + const check = () => { + const cardArea = workspace.getBlock(card).getBodyArea(); + const sectionArea = workspace.getBlock(section).getBodyArea(); + const buttonArea = workspace.getBlock(button).getBodyArea(); + + // Section has to take all width on card, and all height minus separation + expect(sectionArea.x).toEqual(cardArea.x); + expect(sectionArea.width).toEqual(cardArea.width); + + expect(sectionArea.y) + .toBe(cardArea.y + SEPARATION, + `${sectionArea.y} =/= ${cardArea.y} + ${SEPARATION}`); + expect(sectionArea.height + SEPARATION * 2) + .toBe(cardArea.height, + `${sectionArea.height} + ${SEPARATION} * 2 =/= ${cardArea.height}`); + + // Button must be SEPARATION to the left & top of the section + expect(sectionArea.y + SEPARATION) + .toBe(buttonArea.y, + `${sectionArea.y} + ${SEPARATION} =/= ${buttonArea.y}`); + + expect(sectionArea.x + SEPARATION) + .toBe(buttonArea.x, + `${sectionArea.x} + ${SEPARATION} =/= ${buttonArea.x}`); + + expect(buttonArea.y + buttonArea.height + SEPARATION) + .toBe(sectionArea.y + sectionArea.height, + `${buttonArea.y} + ${buttonArea.height} + ${SEPARATION} =/= ${sectionArea.y} + ${sectionArea.height}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + // ---------------------------- + // Link area tests + // ---------------------------- + it('Link Areas should have a stable position', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: "link_area", + x: 10, y: 10, + contents: [ + { + type: 'horizontal_ui_section', + x: 10, y: 10, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + }, + ], + }, + ]); + + const [ link, section, button ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(blocks.length + 1); + /** + * Layout: + * +--------------+ + * |+------------+| + * || +--------+ || + * || | Button | || + * || +--------+ || + * || Horizontal || + * || Section || + * |+------------+| + * | LinkArea | + * +--------------+ + * + **/ + + + const check = () => { + const linkArea = workspace.getBlock(link).getBodyArea(); + const sectionArea = workspace.getBlock(section).getBodyArea(); + const buttonArea = workspace.getBlock(button).getBodyArea(); + const pageArea = workspace.getBlock(page).getBodyArea(); + + // Link area has to has to take all width (minus separation) + expect(linkArea.x) + .toBe(pageArea.x + SEPARATION, + `${linkArea.x} =/= ${pageArea.x} + ${SEPARATION}`); + expect(linkArea.width + SEPARATION * 2) + .toBe(pageArea.width, + `${linkArea.width} + ${SEPARATION} * 2 =/= ${pageArea.width}`); + + // Section has to take all width on link, and all height minus separation + expect(sectionArea.x).toEqual(linkArea.x); + expect(sectionArea.width).toEqual(linkArea.width); + + expect(sectionArea.y) + .toBe(linkArea.y + SEPARATION, + `${sectionArea.y} =/= ${linkArea.y} + ${SEPARATION}`); + expect(sectionArea.height + SEPARATION * 2) + .toBe(linkArea.height, + `${sectionArea.height} + ${SEPARATION} * 2 =/= ${linkArea.height}`); + + // Button must be SEPARATION to the left & top of the section + expect(sectionArea.y + SEPARATION) + .toBe(buttonArea.y, + `${sectionArea.y} + ${SEPARATION} =/= ${buttonArea.y}`); + + expect(sectionArea.x + SEPARATION) + .toBe(buttonArea.x, + `${sectionArea.x} + ${SEPARATION} =/= ${buttonArea.x}`); + + expect(buttonArea.y + buttonArea.height + SEPARATION) + .toBe(sectionArea.y + sectionArea.height, + `${buttonArea.y} + ${buttonArea.height} + ${SEPARATION} =/= ${sectionArea.y} + ${sectionArea.height}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + it('Link Areas should have a stable position even out of Pages', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, _page] = pageGraph([ + { + type: "link_area", + x: 10, y: 10, + contents: [ + { + type: 'horizontal_ui_section', + x: 10, y: 10, + contents: [ + { + type: 'simple_button', + x: 10, y: 10, + }, + ], + }, + ], + }, + ], { noPage: true }); + + const [ link, section, button ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(blocks.length); + /** + * Layout: + * +--------------+ + * |+------------+| + * || +--------+ || + * || | Button | || + * || +--------+ || + * || Horizontal || + * || Section || + * |+------------+| + * | LinkArea | + * +--------------+ + * + **/ + + const check = () => { + const linkArea = workspace.getBlock(link).getBodyArea(); + const sectionArea = workspace.getBlock(section).getBodyArea(); + const buttonArea = workspace.getBlock(button).getBodyArea(); + + // Section has to take all width on link, and all height minus separation + expect(sectionArea.x).toEqual(linkArea.x); + expect(sectionArea.width).toEqual(linkArea.width); + + expect(sectionArea.y) + .toBe(linkArea.y + SEPARATION, + `${sectionArea.y} =/= ${linkArea.y} + ${SEPARATION}`); + expect(sectionArea.height + SEPARATION * 2) + .toBe(linkArea.height, + `${sectionArea.height} + ${SEPARATION} * 2 =/= ${linkArea.height}`); + + // Button must be SEPARATION to the left & top of the section + expect(sectionArea.y + SEPARATION) + .toBe(buttonArea.y, + `${sectionArea.y} + ${SEPARATION} =/= ${buttonArea.y}`); + + expect(sectionArea.x + SEPARATION) + .toBe(buttonArea.x, + `${sectionArea.x} + ${SEPARATION} =/= ${buttonArea.x}`); + + expect(buttonArea.y + buttonArea.height + SEPARATION) + .toBe(sectionArea.y + sectionArea.height, + `${buttonArea.y} + ${buttonArea.height} + ${SEPARATION} =/= ${sectionArea.y} + ${sectionArea.height}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); +}); diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/07-position-responsive-container-inside-section.spec.ts b/frontend/src/app/tests/graphic/ui-element-positioning/07-position-responsive-container-inside-section.spec.ts new file mode 100644 index 00000000..41e9ff8f --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/07-position-responsive-container-inside-section.spec.ts @@ -0,0 +1,239 @@ +import { async, TestBed } from '@angular/core/testing'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { FlowWorkspace } from '../../../flow-editor/flow_workspace'; +import { SEPARATION } from 'app/flow-editor/ui-blocks/renderers/positioning'; +import { configureTestBed, pageGraph } from './builder'; +import { doesNotChangePositionsOnReposition } from './utils'; + +describe('FlowUI positioning: 07. Position responsive container inside section.', () => { + + beforeEach(async(() => { + configureTestBed(TestBed); + })); + + // ---------------------------- + // UI Card tests + // ---------------------------- + it('Simple UI cards should have a stable position', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: 'horizontal_ui_section', + x: 10, y: 10, + contents: [ + { + type: "simple_button", + x: 10, y: 10, + }, + { + type: "simple_card", + x: 11, y: 10, + contents: [], + extra: { dimensions: { width: 200, height: 200, } } + }, + { + type: "simple_button", + x: 12, y: 10, + }, + ], + }, + ]); + + const [ section, leftButton, card, rightButton ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(blocks.length + 1); + /** + * Layout: + * + * +------------------------------------------------+ + * | +-------------+ +-----------+ +--------------+ | + * | | Left button | | Ui Card | | Right button | | + * | +-------------+ +-----------+ +--------------+ | + * | Horizontal Section | + * +------------------------------------------------+ + * + * + **/ + + const check = () => { + const pageArea = workspace.getBlock(page).getBodyArea(); + const sectionArea = workspace.getBlock(section).getBodyArea(); + const leftButtonArea = workspace.getBlock(leftButton).getBodyArea(); + const cardArea = workspace.getBlock(card).getBodyArea(); + const rightButtonArea = workspace.getBlock(rightButton).getBodyArea(); + + // Section has to take all width on page, and all height minus separation + expect(sectionArea.x).toEqual(pageArea.x); + expect(sectionArea.width).toEqual(pageArea.width); + + for (const [name, elem] of [['left button', leftButton], ['center card', card], ['right button', rightButton]]) { + const eArea = workspace.getBlock(elem).getBodyArea(); + + // Height + expect(eArea.y) + .toBeGreaterThanOrEqual(sectionArea.y + SEPARATION, + `Element: ${name}. ${eArea.y} < ${sectionArea.y} + ${SEPARATION}`); + + expect(eArea.y + eArea.height + SEPARATION) + .toBeLessThanOrEqual(sectionArea.y + sectionArea.height, + `Element: ${name}. ${eArea.y} + ${eArea.height} + ${SEPARATION} > ${sectionArea.y} + ${sectionArea.height}`); + } + + for (const [name, left, right ] of [['left -> card', leftButton, card], ['card -> right', card, rightButton]]) { + const leftArea = workspace.getBlock(left).getBodyArea(); + const rightArea = workspace.getBlock(right).getBodyArea(); + + // Relative positioning + expect(leftArea.x + leftArea.width + SEPARATION) + .toBe(rightArea.x, + `Group ${name}. ${leftArea.x} + ${leftArea.width} + ${SEPARATION} =/= ${rightArea.x}`); + + + } + + expect(leftButtonArea.x) + .toBe(sectionArea.x + SEPARATION, + `Left button X. ${leftButtonArea.x} =/= ${sectionArea.x} + ${SEPARATION}`); + + expect(rightButtonArea.x + rightButtonArea.width + SEPARATION) + .toBe(sectionArea.x + sectionArea.width, + `Right button X. ${rightButtonArea.x} + ${rightButtonArea.width} + ${SEPARATION} =/= ${sectionArea.x} + ${sectionArea.width}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); + + // ---------------------------- + // Link Area tests + // ---------------------------- + it('LinkAreas should have a stable position', async () => { + const fixture = TestBed.createComponent(FlowEditorComponent); + + const app: FlowEditorComponent = fixture.debugElement.componentInstance; + expect(app).toBeInstanceOf(FlowEditorComponent); + await app.ngOnInit(); + + const workspace = app.workspace; + expect(workspace).toBeInstanceOf(FlowWorkspace); + + const [graph, blocks, page] = pageGraph([ + { + type: 'horizontal_ui_section', + x: 10, y: 10, + contents: [ + { + type: "simple_button", + x: 10, y: 10, + }, + { + type: "link_area", + x: 11, y: 10, + contents: [], + extra: { dimensions: { width: 200, height: 200, } } + }, + { + type: "simple_button", + x: 12, y: 10, + }, + ], + }, + ]); + + const [ section, leftButton, link, rightButton ] = blocks; + + workspace.load(graph); + workspace.repositionAll(); + workspace.center(); + + const result = workspace.getGraph(); + + expect(Object.keys(result.nodes).length).toBe(blocks.length + 1); + /** + * Layout: + * + * +-------------------------------------------------+ + * | +-------------+ +------------+ +--------------+ | + * | | Left button | | Link Area | | Right button | | + * | +-------------+ +------------+ +--------------+ | + * | Horizontal Section | + * +-------------------------------------------------+ + * + * + **/ + + const check = () => { + const pageArea = workspace.getBlock(page).getBodyArea(); + const sectionArea = workspace.getBlock(section).getBodyArea(); + const leftButtonArea = workspace.getBlock(leftButton).getBodyArea(); + const linkArea = workspace.getBlock(link).getBodyArea(); + const rightButtonArea = workspace.getBlock(rightButton).getBodyArea(); + + // Section has to take all width on page, and all height minus separation + expect(sectionArea.x).toEqual(pageArea.x); + expect(sectionArea.width).toEqual(pageArea.width); + + for (const [name, elem] of [['left button', leftButton], ['center linkArea', link], ['right button', rightButton]]) { + const eArea = workspace.getBlock(elem).getBodyArea(); + + // Height + expect(eArea.y) + .toBeGreaterThanOrEqual(sectionArea.y + SEPARATION, + `Element: ${name}. ${eArea.y} < ${sectionArea.y} + ${SEPARATION}`); + + expect(eArea.y + eArea.height + SEPARATION) + .toBeLessThanOrEqual(sectionArea.y + sectionArea.height, + `Element: ${name}. ${eArea.y} + ${eArea.height} + ${SEPARATION} > ${sectionArea.y} + ${sectionArea.height}`); + } + + for (const [name, left, right ] of [['left -> linkArea', leftButton, link], ['linkArea -> right', link, rightButton]]) { + const leftArea = workspace.getBlock(left).getBodyArea(); + const rightArea = workspace.getBlock(right).getBodyArea(); + + // Relative positioning + expect(leftArea.x + leftArea.width + SEPARATION) + .toBe(rightArea.x, + `Group ${name}. ${leftArea.x} + ${leftArea.width} + ${SEPARATION} =/= ${rightArea.x}`); + + + } + + expect(leftButtonArea.x) + .toBe(sectionArea.x + SEPARATION, + `Left button X. ${leftButtonArea.x} =/= ${sectionArea.x} + ${SEPARATION}`); + + expect(rightButtonArea.x + rightButtonArea.width + SEPARATION) + .toBe(sectionArea.x + sectionArea.width, + `Right button X. ${rightButtonArea.x} + ${rightButtonArea.width} + ${SEPARATION} =/= ${sectionArea.x} + ${sectionArea.width}`); + }; + + // Check once + check(); + + // Re-position to check for stability + doesNotChangePositionsOnReposition(workspace, blocks); + + // Check again + check(); + }); +}); diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/builder.ts b/frontend/src/app/tests/graphic/ui-element-positioning/builder.ts new file mode 100644 index 00000000..01150e79 --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/builder.ts @@ -0,0 +1,159 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBedStatic } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CookiesService } from '@ngx-utils/cookies'; +import { BrowserCookiesModule, BrowserCookiesService } from '@ngx-utils/cookies/browser'; +import { ConnectionService } from 'app/connection.service'; +import { CustomBlockService } from 'app/custom_block.service'; +import { ProgramService } from 'app/program.service'; +import { ServiceService } from 'app/service.service'; +import { UiSignalService } from 'app/services/ui-signal.service'; +import { FlowEditorComponent } from '../../../flow-editor/flow-editor.component'; +import { SessionService } from '../../../session.service'; +import { FakeConnectionService } from './fake-connection.service'; +import { FakeCustomBlockService } from './fake-custom-block.service'; +import { FakeProgramService } from './fake-program.service'; +import { FakeServiceService } from './fake-service.service'; +import { FakeSessionService } from './fake-session.service'; +import { FakeUiSignalService } from './fake-ui-signal.service'; +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { uuidv4 } from '../../../flow-editor/utils'; +import { UiElementWidgetType } from '../../../flow-editor/ui-blocks/renderers/ui_tree_repr'; +import { UiFlowBlockExtraData } from '../../../flow-editor/ui-blocks/ui_flow_block'; +import { ToastrModule } from 'ngx-toastr'; + +export function configureTestBed(testBed: TestBedStatic) { + testBed.configureTestingModule({ + imports: [ + BrowserCookiesModule.forRoot(), + RouterTestingModule, + HttpClientTestingModule, + MatDialogModule, + MatSnackBarModule, + MatMenuModule, + NoopAnimationsModule, + ToastrModule.forRoot(), + ], + declarations: [ + FlowEditorComponent, + ], + providers: [ + { + provide: CookiesService, + useClass: BrowserCookiesService, + }, + { + provide: HttpClient, + useValue: {} + }, + { + provide: SessionService, + useClass: FakeSessionService, + }, + { + provide: ProgramService, + useClass: FakeProgramService, + }, + { + provide: UiSignalService, + useClass: FakeUiSignalService, + }, + { + provide: ConnectionService, + useClass: FakeConnectionService, + }, + { + provide: ServiceService, + useClass: FakeServiceService, + }, + { + provide: CustomBlockService, + useClass: FakeCustomBlockService, + } + ] + }); +} + +type TestGraphElement = { + type: UiElementWidgetType, + x: number, y: number, + contents?: TestGraphElement[], + extra?: UiFlowBlockExtraData, +}; + +export function pageGraph(elements: TestGraphElement[], options?: { noPage: boolean }): [FlowGraph, string[], string] { + const graph: FlowGraph = { edges: [], nodes: {} }; + + if (!options) { + options = { noPage: false }; + } + + const pageId = options.noPage ? null : uuidv4(); + + if (pageId) { + graph.nodes[pageId] = { + data: { + value: { + "options": { + "type": "ui_flow_block", + "subtype": "container_flow_block", + "outputs": [], + "isPage": true, + "inputs": [], + "id": "responsive_page_holder", + }, + extra: {} + }, + type: "ui_flow_block", + subtype: "container_flow_block" + }, + container_id: null, + position: { x: 0, y: 0 }, + }; + } + + const ids = []; + const todo: (TestGraphElement & { container?: string })[] = elements.concat(); + + while (todo.length > 0) { + const el = todo.shift(); + + const id = uuidv4(); + ids.push(id); + + graph.nodes[id] = { + data: { + "value": { + "options": { + "type": "ui_flow_block", + "id": el.type, + }, + }, + "type": "ui_flow_block" + }, + container_id: el.container ? el.container : pageId, + position: { x: el.x, y: el.y } + } + + if (el.extra) { + graph.nodes[id].data.value.extra = el.extra; + } + + if (el.contents) { + graph.nodes[id].data.subtype = 'container_flow_block'; + + for (const subEl of el.contents.concat([]).reverse()) { + (subEl as any).container = id; + todo.unshift(subEl); + } + + } + } + + return [graph, ids, pageId]; +} diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/fake-connection.service.ts b/frontend/src/app/tests/graphic/ui-element-positioning/fake-connection.service.ts new file mode 100644 index 00000000..55f4365a --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/fake-connection.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@angular/core"; +import { BridgeIndexData } from "app/bridges/bridge"; +import { BridgeConnection } from "app/connection"; + +@Injectable() +export class FakeConnectionService { + constructor() {} + + public getAvailableBridgesForNewConnectionOnProgram(programId: string): Promise { + return Promise.resolve([]); + } + + public getConnectionsOnProgram(programId: string): Promise { + return Promise.resolve([]); + } +} diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/fake-custom-block.service.ts b/frontend/src/app/tests/graphic/ui-element-positioning/fake-custom-block.service.ts new file mode 100644 index 00000000..f0b62367 --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/fake-custom-block.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from "@angular/core"; +import { ResolvedCustomBlock } from "app/custom_block"; + +@Injectable() +export class FakeCustomBlockService { + constructor() {} + + public async getCustomBlocksOnProgram(programId: string, skip_resolve_argument_options?: boolean): Promise { + return Promise.resolve([]); + } +} diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/fake-program.service.ts b/frontend/src/app/tests/graphic/ui-element-positioning/fake-program.service.ts new file mode 100644 index 00000000..e8088afa --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/fake-program.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from "@angular/core"; +import { ProgramContent, ProgramInfoUpdate } from "app/program"; +import { Observable, Observer } from "rxjs"; + +const EMPTY_PROGRAM_GETTER: ((programId: string) => Promise) + = (programId: string) => { + return Promise.resolve({ + id: programId, + name: 'empty program', + enabled: true, + bridges_in_use: [], + visibility: 'private', + + type: 'flow_program', + parsed: null, + orig: { nodes: {}, edges: [] }, + owner: null, + owner_full: { type: 'user', id: null }, + }); +} + +@Injectable() +export class FakeProgramService { + private _programGetter: (programId: string) => Promise; + private _observers: Observer[] = []; + + constructor() { + this._programGetter = EMPTY_PROGRAM_GETTER; + } + + setProgramToBePulled(getter: ((programId: string) => Promise) ) { + this._programGetter = getter; + } + + getProgramById(programId: string): Promise { + return this._programGetter(programId); + } + + getAssetUrlOnProgram(assetId: string, programId: string): string { + return `http://localhost:9999/programs/by-id/${programId}/assets/by-id/${assetId}`; + } + + watchProgramLogs(programId: string, options: { request_previous_logs?: boolean }): Observable { + return new Observable(observer => { + this._observers.push(observer) + }); + } +} diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/fake-service.service.ts b/frontend/src/app/tests/graphic/ui-element-positioning/fake-service.service.ts new file mode 100644 index 00000000..97e6f23d --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/fake-service.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from "@angular/core"; +import { AvailableService } from "app/service"; + +@Injectable() +export class FakeServiceService { + constructor() {} + + getAvailableServicesOnProgram(programId: string): Promise { + return Promise.resolve([]); + } +} diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/fake-session.service.ts b/frontend/src/app/tests/graphic/ui-element-positioning/fake-session.service.ts new file mode 100644 index 00000000..f88badcd --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/fake-session.service.ts @@ -0,0 +1,20 @@ +import { Session } from "app/session"; +import { Injectable } from "@angular/core"; + +@Injectable() +export class FakeSessionService /* implements SessionService */ { + constructor() {} + + getSession(): Promise { + return Promise.resolve({ + username: 'fake-username', + user_id: '123-fake', + active: true, + tags: { + is_admin: false, + is_advanced: false, + is_in_preview: false, + } + }); + } +} diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/fake-ui-signal.service.ts b/frontend/src/app/tests/graphic/ui-element-positioning/fake-ui-signal.service.ts new file mode 100644 index 00000000..36c3e817 --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/fake-ui-signal.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@angular/core"; +import { Observable, Observer } from "rxjs"; + +@Injectable() +export class FakeUiSignalService { + programId: string; + observers: Observer[] = []; + + setProgramId(programId: string) { + this.programId = programId; + } + + public onElementUpdate(blockType: string, blockId: string): Observable { + return new Observable(observer => { this.observers.push(observer); }); + } +} diff --git a/frontend/src/app/tests/graphic/ui-element-positioning/utils.ts b/frontend/src/app/tests/graphic/ui-element-positioning/utils.ts new file mode 100644 index 00000000..8166b2ce --- /dev/null +++ b/frontend/src/app/tests/graphic/ui-element-positioning/utils.ts @@ -0,0 +1,39 @@ +import { FlowWorkspace } from "app/flow-editor/flow_workspace"; +import { Area2D } from "app/flow-editor/flow_block"; +import { maxKey } from "app/flow-editor/utils"; + +export function doesNotChangePositionsOnReposition(workspace: FlowWorkspace, blocks: string[]) { + // Get all block's positions + const prevPos: [string, Area2D][] = blocks.map( id => [id, workspace.getBlock(id).getBodyArea() ] ); + + workspace.repositionAll(); + + const diffs = prevPos.map(([id, prev]) => { + const after = workspace.getBlock(id).getBodyArea(); + + return { + id: id, + 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 x = maxKey(diffs, (e => e.x)); + const y = maxKey(diffs, (e => e.x)); + const width = maxKey(diffs, (e => e.width)); + const height = maxKey(diffs, (e => e.height)); + + const mov = { + x: { id: x.id, v: x.x }, + y: { id: y.id, v: y.y }, + width: { id: width.id, v: width.width }, + height: { id: height.id, v: height.height }, + }; + + if ((mov.x.v >= 1) || (mov.y.v >= 1) || (mov.width.v >= 1) || (mov.height.v >= 1)) { + // @ts-ignore Avoid importing this from assert() as that one has 2 more parameters + fail("Movement on iteration that should not change positions: " + JSON.stringify(mov)); + } +} diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/.gitignore b/frontend/src/app/tests/logic/flow-graph-analysis/.gitignore new file mode 100644 index 00000000..b38f8050 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/.gitignore @@ -0,0 +1,2 @@ +*.dot +*.png diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/01_simple_flow.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/01_simple_flow.spec.ts new file mode 100644 index 00000000..52b3c78d --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/01_simple_flow.spec.ts @@ -0,0 +1,248 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile, get_conversions_to_stepped, get_stepped_ast, get_tree_with_ends, get_unreachable } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled, SimpleArrayAstOperation } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +// @ts-ignore +import * as _01_simple_flow from '../samples/01_simple_flow.js'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: options.source_id, message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source, 1], [source, 2], 0]}); + const cond2 = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + builder.add_trigger('trigger_when_all_true', {id: 'trigger', args: [[cond1, 0], [cond2, 0]]}) + .then(f => f.add_op('send_message', { namespace: chat, + args: [channel, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-01: Simple flow.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should find no unreachable blocks', async () => { + expect(get_unreachable(gen_flow())).toEqual([]); + }); + + it('Should recognize conversions to stepped', async () => { + expect(get_conversions_to_stepped(gen_flow({source_id: 'source'}), "source")).toEqual([ + "trigger", + ]); + }); + + it('Synthetic compilation matches DSL program', async () => { + const TIME_BLOCK = "ad97e5d1-c725-4cc6-826f-30057f239635"; + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + are_equivalent_ast(compile(gen_flow({ source_id: TIME_BLOCK })), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "${TIME_BLOCK}" 0) + 11) + (= (flow-last-value "${TIME_BLOCK}" 1) + (flow-last-value "${TIME_BLOCK}" 2) + 0)) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))))) + `)) + ]); + }); + + // Based on sample + describe('Sample-based tests.', async () => { + it('Sample should match DSL compilation', async () => { + const TIME_BLOCK = "ad97e5d1-c725-4cc6-826f-30057f239635"; + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + are_equivalent_ast(compile(_01_simple_flow as FlowGraph), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "${TIME_BLOCK}" 0) + 11) + (= (flow-last-value "${TIME_BLOCK}" 1) + (flow-last-value "${TIME_BLOCK}" 2) + 0)) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))))) + `)) + ]); + }); + + it('Sample should match synthetic compilation', async () => { + are_equivalent_ast(compile(_01_simple_flow as FlowGraph), + compile(gen_flow({source_id: 'ad97e5d1-c725-4cc6-826f-30057f239635'}))); + }); + + // Intermediate tests based on sample, might be removed if they prove to + // costly to maintain + it('Should build correctly the stepper AST from sample', async () => { + expect(get_stepped_ast(_01_simple_flow as FlowGraph, "032d2a4e-bfe2-4635-a1cf-dc62692eead7")) + .toEqual([ + { + block_id: "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + arguments: [ + { + tree: { + block_id: "f1b8670c-0001-4417-8a39-2c52f5140383", + arguments: [] + }, + output_index: 0, + }, + { + tree: { + block_id: "918294c3-1e7d-4b2f-ab73-77188a4b89b0", + arguments: [ + { + tree: { + block_id: "fc4bd63f-d4ff-4e15-8b09-a224e6e1c635", + arguments: [], + }, + output_index: 0 + } + ] + }, + output_index: 0, + } + ], + contents: [] + } + ]); + }); + + it('Sample should build correctly the streaming tree', async () => { + expect(get_tree_with_ends(_01_simple_flow as FlowGraph, + "ad97e5d1-c725-4cc6-826f-30057f239635", + "032d2a4e-bfe2-4635-a1cf-dc62692eead7")) + .toEqual({ + block_id: "032d2a4e-bfe2-4635-a1cf-dc62692eead7", + arguments: [ + { // Hour = 11 + tree: { + block_id: "0f82640f-b40b-4053-981d-1fe0b2c17de0", + arguments: [ + { // Hour + tree: { + block_id: "ad97e5d1-c725-4cc6-826f-30057f239635", + arguments: [], + }, + output_index: 0 + }, + { // "11" + tree: { + block_id: "0b2f1836-aaf3-4e13-84b9-8041c3b5b4b8", + arguments: [], + }, + output_index: 0, + } + ] + }, + output_index: 0 + }, + { // Minute = Second = 0 + tree: { + block_id: "0d4937a0-cdd3-4bf6-b8e8-8da2974b1330", + arguments: [ + { + tree: { + block_id: "ad97e5d1-c725-4cc6-826f-30057f239635", + arguments: [], + }, + output_index: 1 + }, + { + tree: { + block_id: "ad97e5d1-c725-4cc6-826f-30057f239635", + arguments: [], + }, + output_index: 2 + }, + { + tree: { + block_id: "2b3b54d4-bcfa-4137-825f-ca52e2be4e96", + arguments: [], + }, + output_index: 0, + } + ] + }, + output_index: 0 + } + ] + }); + }); + }); + + // Same, but testing intermediate scaffoling, might be removed if too costly + describe('Scaffold step testing.', async () => { + it('Should match tool compilation', async () => { + are_equivalent_ast(compile(_01_simple_flow as FlowGraph), [ + gen_compiled([ + ['wait_for_monitor', { monitor_id: { from_service: TIME_MONITOR_ID }, key: 'utc_time', monitor_expected_value: 'any_value' }], + ['control_if_else', + ['operator_and', ['operator_equals', + ['flow_last_value', 'ad97e5d1-c725-4cc6-826f-30057f239635', 0], + 11], + ['operator_equals', + ['flow_last_value', 'ad97e5d1-c725-4cc6-826f-30057f239635', 1], + ['flow_last_value', 'ad97e5d1-c725-4cc6-826f-30057f239635', 2], + 0 + ] + ] as SimpleArrayAstOperation, + [ + ['command_call_service', { + service_id: 'de5baefb-13da-457e-90a5-57a753da8891', + service_action: 'send_message', + service_call_values: ['-137414823', + ['command_call_service', { + service_id: '536bf266-fabf-44a6-ba89-a0c71b8db608', + service_action: 'get_today_max_in_place', + service_call_values: ["12/36/057/7"] + }]], + }] + ] + ] + ]) + ]); + }); + }); + +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/02_lone_block.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/02_lone_block.spec.ts new file mode 100644 index 00000000..8b5052d4 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/02_lone_block.spec.ts @@ -0,0 +1,39 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { get_unreachable } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +// @ts-ignore +import * as _02_lone_block from '../samples/02_lone_block.js'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + builder.add_op('send_message', { id: 'lone', namespace: chat }); + + const graph = builder.build(); + return graph; +} + +describe('Flow-02: Lone block.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Unreachable blocks?.*/i) + }); + + it('Should find an unreachable block', async () => { + expect(get_unreachable(gen_flow())).toEqual([ + "lone" + ]); + }); + + describe('Sample-based tests.', async () => { + it('Should find an unreachable block', async () => { + expect(get_unreachable(_02_lone_block as FlowGraph)).toEqual([ + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c" + ]); + }); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/03_no_start_pulse.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/03_no_start_pulse.spec.ts new file mode 100644 index 00000000..56989492 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/03_no_start_pulse.spec.ts @@ -0,0 +1,59 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { get_unreachable } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +// @ts-ignore +import * as _03_no_start_pulse from '../samples/03_no_start_pulse.js'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7', { id: 'loc'}); + const channel = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823', {id: 'channel'}); + + // Stepped section + builder.add_op('send_message', { namespace: chat, + id: 'not-started', + args: [channel, + [(b) => b.add_getter('get_today_max_in_place', { id: 'getter', + namespace: weather, + args: [loc] + }), 0]] + }); + + const graph = builder.build(); + return graph; +} + +describe('Flow-03: No start pulse.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Unreachable blocks?.*/i) + }); + + it('Should find an unreachable blocks', async () => { + expect(get_unreachable(gen_flow()).sort()).toEqual([ + "not-started", + "getter", + "loc", + "channel", + ].sort()); + }); + + describe('Sample-based tests.', async () => { + it('Should find unreachable blocks', async () => { + expect(get_unreachable(_03_no_start_pulse as FlowGraph).sort()).toEqual([ + "4652b79c-603b-4add-9164-92508be43fdf", + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "f1b8670c-0001-4417-8a39-2c52f5140383", + "099ee247-851d-41bb-819c-5347247cd06a", + ].sort()); + }); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/04_no_start_loop.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/04_no_start_loop.spec.ts new file mode 100644 index 00000000..b3c016e6 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/04_no_start_loop.spec.ts @@ -0,0 +1,72 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { get_unreachable } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +// @ts-ignore +import * as _04_no_start_loop from '../samples/04_no_start_loop.js'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7', {id: 'loc'}); + const channel = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823', {id: 'channel'}); + + // Stepped section + const msg1 = builder.add_op('send_message', { namespace: chat, + id: 'first', + args: [channel, + [(b) => b.add_getter('get_today_max_in_place', { id: 'first-getter', + namespace: weather, + args: [loc] + }), 0]] + }); + + const msg2 = builder.add_op('send_message', { namespace: weather, + id: 'second', + args: [channel, + [(b) => b.add_getter('get_today_max_in_place', { id: 'second-getter', + namespace: weather, + args: [loc] + }), 0]] + }); + + msg1.then(msg2).then(msg1); + + const graph = builder.build(); + return graph; +} + +describe('Flow-04: No start loop.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Unreachable blocks?.*/i) + }); + + it('Should find an unreachable blocks', async () => { + expect(get_unreachable(gen_flow()).sort()).toEqual([ + "first", + "second", + "first-getter", + "second-getter", + "loc", + "channel" + ].sort()); + }); + + describe('Sample-based tests.', async () => { + it('Should find unreachable blocks', async () => { + expect(get_unreachable(_04_no_start_loop as FlowGraph).sort()).toEqual([ + "4652b79c-603b-4add-9164-92508be43fdf", + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "f1b8670c-0001-4417-8a39-2c52f5140383", + "099ee247-851d-41bb-819c-5347247cd06a", + ].sort()); + }); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/05_multiple_stream_in.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/05_multiple_stream_in.spec.ts new file mode 100644 index 00000000..dcafbb5d --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/05_multiple_stream_in.spec.ts @@ -0,0 +1,106 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile, get_conversions_to_stepped, get_source_signals, get_unreachable } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + + // Stream section + const source1 = builder.add_stream('flow_utc_time', {id: 'source1', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source1, 0], 11]}); + + const source2 = builder.add_stream('flow_utc_time', {id: 'source2', message: 'UTC time'}); + const cond2 = builder.add_stream('operator_equals', {args: [[source2, 1], [source2, 2], 0]}); + + // Stepped section + builder.add_trigger('trigger_when_all_true', {id: 'trigger', args: [[cond1, 0], [cond2, 0]]}) + .then(f => f.add_op('send_message', { namespace: chat, + args: [channel, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-05: Multiple stream in.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should find no unreachable blocks', async () => { + expect(get_unreachable(gen_flow())).toEqual([]); + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + are_equivalent_ast(compile(gen_flow()), + [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source1" 0) + 11) + (= (flow-last-value "source2" 1) + (flow-last-value "source2" 2) + 0)) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))))) + ` + )), + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source1" 0) + 11) + (= (flow-last-value "source2" 1) + (flow-last-value "source2" 2) + 0)) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))))) + ` + )) + ]); + }); + + describe('Intermediate step tests.', async () => { + it('Should recognize source signals', async () => { + expect(get_source_signals(gen_flow())).toEqual([ + "source1", + "source2", + ]); + }); + + it('Should recognize conversions to stepped', async () => { + expect(get_conversions_to_stepped(gen_flow(), "source1")).toEqual([ + "trigger", + ]); + }); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/06_stepped_loops.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/06_stepped_loops.spec.ts new file mode 100644 index 00000000..f9fb3c66 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/06_stepped_loops.spec.ts @@ -0,0 +1,78 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + const cond2 = builder.add_stream('operator_equals', {args: [[source, 1], [source, 2], 0]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {id: 'trigger', args: [[cond1, 0], [cond2, 0]]}) + const send_message = builder.add_op('send_message', { namespace: chat, + id: 'loop-start', + args: [channel, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + const wait_1sec = builder.add_op('control_wait', { args: [1] + }); + + trigger.then(send_message).then(wait_1sec).then(send_message).then(wait_1sec); + + const graph = builder.build(); + return graph; +} + +describe('Flow-06: Stepped loops.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + are_equivalent_ast(compile(gen_flow()), + [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + (= (flow-last-value "source" 1) + (flow-last-value "source" 2) + 0)) + ((jump-point "loop-start") + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (wait-seconds 1) + (jump-to "loop-start") + )) + ` + )), + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/07_streaming_loops.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/07_streaming_loops.spec.ts new file mode 100644 index 00000000..9424d7a4 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/07_streaming_loops.spec.ts @@ -0,0 +1,33 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('camera1', { namespace: 'tts', + id: 'source', message: 'UTC time'}); + const sub = builder.add_stream('sub', { namespace: 'tts', + args: [[source, 0], ['detector', 0]]}); + const noise_detector = builder.add_stream('noise_detection', { namespace: 'tts', + id: 'detector', args: [[sub, 0]]}); + + // Stepped section + const trigger = builder.add_trigger('on_word_detection', { namespace: 'tts', + id: 'trigger', args: [[noise_detector, 0]]}); + const op = builder.add_op('show_string', { args: [[trigger, 1]] + }); + + trigger.then(op); + + const graph = builder.build(); + return graph; +} + +describe('Flow-07: Streaming loops.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*loops? in streaming section.*/i); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/08_stepped_branching.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/08_stepped_branching.spec.ts new file mode 100644 index 00000000..d6075474 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/08_stepped_branching.spec.ts @@ -0,0 +1,86 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel1 = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + const channel2 = builder.add_enum_node(chat, 'get_known_channels', 'Personal', '-137414824'); + + // Stream section + const source = builder.add_stream('flow_utc_time', {'id': 'source', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source, 1], [source, 2], 0]}); + const cond2 = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond1, 0], [cond2, 0]]}); + const branch1 = builder.add_op('send_message', { namespace: chat, + args: [channel1, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + const branch2 = builder.add_op('send_message', { namespace: chat, + args: [channel2, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + builder.add_fork(trigger, [branch1, branch2]); + + const graph = builder.build(); + return graph; +} + +describe('Flow-08: Stepped branching.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + (= (flow-last-value "source" 1) + (flow-last-value "source" 2) + 0)) + ((fork + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + ))) + ` + )), + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/09_stepped_branch_and_join.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/09_stepped_branch_and_join.spec.ts new file mode 100644 index 00000000..9789c760 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/09_stepped_branch_and_join.spec.ts @@ -0,0 +1,99 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel1 = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + const channel2 = builder.add_enum_node(chat, 'get_known_channels', 'Personal', '-137414824'); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source, 1], [source, 2], 0]}); + const cond2 = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond1, 0], [cond2, 0]]}); + const branch1 = builder.add_op('send_message', { namespace: chat, + args: [channel1, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + const branch2 = builder.add_op('send_message', { namespace: chat, + args: [channel2, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + builder.add_fork(trigger, [branch1, branch2]); + + const joiner = builder.add_trigger('trigger_when_all_completed', {args: [[ branch1, 'pulse' ], [branch2, 'pulse']]}); + joiner.then(f => f.add_op('send_message', { namespace: chat, + args: [channel2, 'completed'] + })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-09: Stepped branch-and-join.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + const join = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + (= (flow-last-value "source" 1) + (flow-last-value "source" 2) + 0)) + ((fork + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + ) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + "completed")))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(join, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/10_different_trigger_common_stepped.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/10_different_trigger_common_stepped.spec.ts new file mode 100644 index 00000000..3ea7b0ce --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/10_different_trigger_common_stepped.spec.ts @@ -0,0 +1,94 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel1 = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + + // First trigger + // Stream section + const source1 = builder.add_stream('flow_utc_time', {id: 'source1', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source1, 0], 11]}); + const trigger1 = builder.add_trigger('trigger_when_all_true', {args: [[cond1, 0]]}); + + + // Second trigger + const source2 = builder.add_stream('flow_utc_time', {id: 'source2', message: 'UTC time'}); + const cond2 = builder.add_stream('operator_equals', {args: [[source2, 1], [source2, 2], 0]}); + const trigger2 = builder.add_trigger('trigger_when_all_true', {args: [[cond2, 0]]}); + + // Stepped section + const body = builder.add_op('send_message', { namespace: chat, + args: [channel1, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + trigger1.then(body); + trigger2.then(body); + + const graph = builder.build(); + return graph; +} + +describe('Flow-10: Different trigger common stepped.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + const join = compile(gen_flow()); + + const dsl_ast_branch1 = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source1" 0) + 11) + ) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))))) + ` + ); + + const dsl_ast_branch2 = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source2" 1) + (flow-last-value "source2" 2) + 0)) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast_branch1), gen_compiled(dsl_ast_branch2)]; + + are_equivalent_ast(join, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/11_common_stepped_ending.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/11_common_stepped_ending.spec.ts new file mode 100644 index 00000000..978a24a9 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/11_common_stepped_ending.spec.ts @@ -0,0 +1,120 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel1 = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + const channel2 = builder.add_enum_node(chat, 'get_known_channels', 'Personal', '-137414824'); + + // First trigger + // Stream section + const source1 = builder.add_stream('flow_utc_time', {id: 'source1', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source1, 0], 11]}); + const trigger1 = builder.add_trigger('trigger_when_all_true', {args: [[cond1, 0]]}); + const body1 = builder.add_op('send_message', { namespace: chat, + args: [channel1, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + // Second trigger + const source2 = builder.add_stream('flow_utc_time', {id: 'source2', message: 'UTC time'}); + const cond2 = builder.add_stream('operator_equals', {args: [[source2, 1], [source2, 2], 0]}); + const trigger2 = builder.add_trigger('trigger_when_all_true', {args: [[cond2, 0]]}); + const body2 = builder.add_op('send_message', { namespace: chat, + args: [channel2, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + // Common end + const ending = builder.add_op('send_message', { namespace: chat, + args: [channel2, + "done"] + }); + + trigger1.then(body1).then(body2); + trigger2.then(body2).then(ending); + + const graph = builder.build(); + return graph; +} + +describe('Flow-11: Common stepped ending.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + const join = compile(gen_flow()); + + const dsl_ast_branch1 = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source1" 0) + 11) + ) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + "done")) + )) + ` + ); + + const dsl_ast_branch2 = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source2" 1) + (flow-last-value "source2" 2) + 0)) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + "done")) + )) + ` + ); + + const from_ast = [gen_compiled(dsl_ast_branch1), gen_compiled(dsl_ast_branch2)]; + + are_equivalent_ast(join, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/12_fork_to_different_branch.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/12_fork_to_different_branch.spec.ts new file mode 100644 index 00000000..3d62f618 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/12_fork_to_different_branch.spec.ts @@ -0,0 +1,132 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel1 = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + const channel2 = builder.add_enum_node(chat, 'get_known_channels', 'Personal', '-137414824'); + + // First trigger + // Stream section + const source1 = builder.add_stream('flow_utc_time', {id: 'source1', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source1, 0], 11]}); + const trigger1 = builder.add_trigger('trigger_when_all_true', {args: [[cond1, 0]]}); + const body1 = builder.add_op('send_message', { namespace: chat, + args: [channel1, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + // Second trigger + const source2 = builder.add_stream('flow_utc_time', {id: 'source2', message: 'UTC time'}); + const cond2 = builder.add_stream('operator_equals', {args: [[source2, 1], [source2, 2], 0]}); + const trigger2 = builder.add_trigger('trigger_when_all_true', {args: [[cond2, 0]]}); + const body2 = builder.add_op('send_message', { namespace: chat, + args: [channel2, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + const body3 = builder.add_op('send_message', { namespace: chat, + args: [channel2, "this in parallel"] + }); + + // Common end + const ending = builder.add_op('send_message', { namespace: chat, + args: [channel2, + "done"] + }); + + trigger1.then(body1).then(body2); + + builder.add_fork(trigger2, [body2, body3]); + body2.then(ending); + + const graph = builder.build(); + return graph; +} + +describe('Flow-12: Fork to different branch.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + const WEATHER_SVC = "536bf266-fabf-44a6-ba89-a0c71b8db608"; + + const join = compile(gen_flow()); + + const dsl_ast_branch1 = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source1" 0) + 11) + ) + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414823" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + "done")) + )) + ` + ); + + const dsl_ast_branch2 = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source2" 1) + (flow-last-value "source2" 2) + 0)) + ((fork + ;; Branch1 + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + (call-service id: "${WEATHER_SVC}" + action: "get_today_max_in_place" + values: ("12/36/057/7")))) + (call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + "done"))) + ;; Branch2 + ((call-service id: "${CHAT_SVC}" + action: "send_message" + values: ("-137414824" + "this in parallel"))) + ))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast_branch1), gen_compiled(dsl_ast_branch2)]; + + are_equivalent_ast(join, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/13_fork_to_different_branch_then_join.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/13_fork_to_different_branch_then_join.spec.ts new file mode 100644 index 00000000..57c75c74 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/13_fork_to_different_branch_then_join.spec.ts @@ -0,0 +1,73 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Services + const weather = builder.add_service('536bf266-fabf-44a6-ba89-a0c71b8db608'); + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Values + const loc = builder.add_enum_node(weather, 'get_locations', 'Vigo', '12/36/057/7'); + const channel1 = builder.add_enum_node(chat, 'get_known_channels', 'Bot testing', '-137414823'); + const channel2 = builder.add_enum_node(chat, 'get_known_channels', 'Personal', '-137414824'); + + // First trigger + // Stream section + const source1 = builder.add_stream('flow_utc_time', {id: 'source1', message: 'UTC time'}); + const cond1 = builder.add_stream('operator_equals', {args: [[source1, 0], 11]}); + const trigger1 = builder.add_trigger('trigger_when_all_true', {args: [[cond1, 0]]}); + const body1 = builder.add_op('send_message', { namespace: chat, + args: [channel1, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + + // Second trigger + const source2 = builder.add_stream('flow_utc_time', {id: 'source2', message: 'UTC time'}); + const cond2 = builder.add_stream('operator_equals', {args: [[source2, 1], [source2, 2], 0]}); + const trigger2 = builder.add_trigger('trigger_when_all_true', {args: [[cond2, 0]]}); + const body2 = builder.add_op('send_message', { namespace: chat, + args: [channel2, + [(b) => b.add_getter('get_today_max_in_place', { namespace: weather, + args: [loc] + }), 0]] + }); + const body3 = builder.add_op('send_message', { namespace: chat, + args: [channel2, "this in parallel"] + }); + + // Common end + const ending = builder.add_op('send_message', { namespace: chat, + args: [channel2, + "done"] + }); + + trigger1.then(body1).then(body2); + + builder.add_fork(trigger2, [body2, body3]); + body2.then(ending); + + const joiner = builder.add_trigger('trigger_when_all_completed', {args: [[ ending, 'pulse' ], [body3, 'pulse']]}); + joiner.then(f => f.add_op('send_message', { namespace: chat, + args: [channel2, 'completed'] + })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-13: Fork to different branch then join.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Joins can only be done between flows that have previously forked.*/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/14_stepped_branch_multiple_complex_joins.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/14_stepped_branch_multiple_complex_joins.spec.ts new file mode 100644 index 00000000..871f74f7 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/14_stepped_branch_multiple_complex_joins.spec.ts @@ -0,0 +1,104 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + const branch3 = builder.add_op('control_wait', { id: 'branch3', + args: [ 3 ] + }); + + const branch4 = builder.add_op('control_wait', { id: 'branch4', + args: [ 4 ] + }); + + const branch5 = builder.add_op('control_wait', { id: 'branch5', + args: [ 5 ] + }); + + const branch6 = builder.add_op('control_wait', { id: 'branch6', + args: [ 6 ] + }); + + builder.add_fork(trigger, [branch1, branch2, branch3, branch4, branch5, branch6 ]); + + // Join branch 1 and 2 + const joiner12 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch1, 'pulse' ], [branch2, 'pulse']]}); + joiner12.then(f => f.add_op('control_wait', { id: 'joiner12', args: [ 11 ] })); + + // Join branch (4 and 5), then operate, then merge with with 3 + const joiner45 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch4, 'pulse' ], [branch5, 'pulse']]}); + const branch4_bot = joiner45.then(f => f.add_op('control_wait', { id: 'branch4-5_bot', + args: [ 4.5 ] })); + + // Join result of merge 4-5 with branch3 + const joiner34 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch3, 'pulse' ], [branch4_bot, 'pulse']]}); + joiner34.then(f => f.add_op('control_wait', { id: 'joiner34', args: [ 12 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-14: Stepped branch with multiple, complex joins.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork + ;; Branches 1 and 2 + ((fork + (control_wait 1) ; Branch 1 + (control_wait 2)) ; Branch 2 + ;; After branch 1 and 2 join + (control_wait 11)) + ;; Branches 3, 4, 5 + ((fork + (control_wait 3) ; Branch 3 + ((fork ; Branches 4 and 5 + (control_wait 4) + (control_wait 5)) + (control_wait 4.5))) ; Join 4 and 5 + ;; Join 3 and 4 + (control_wait 12)) + ;; Branch 6 + (control_wait 6)) + )) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/15_stepped_loop_inside_fork.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/15_stepped_loop_inside_fork.spec.ts new file mode 100644 index 00000000..d4e666f8 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/15_stepped_loop_inside_fork.spec.ts @@ -0,0 +1,85 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + const branch3 = builder.add_op('control_wait', { id: 'branch3', + args: [ 3 ] + }); + + builder.add_fork(trigger, [branch1, branch2, branch3 ]); + + const branch3_loop_helper = builder.add_op('control_wait', { id: 'branch3-loop-helper', + args: [ 3.5 ] + }); + + branch3.then(branch3_loop_helper).then(branch3); + + // Join branch 1 and 2 + const joiner12 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch1, 'pulse' ], [branch2, 'pulse']]}); + joiner12.then(f => f.add_op('control_wait', { id: 'joiner12', args: [ 11 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-15: Stepped loop inside fork.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork + ;; Branches 1 and 2 + ((fork + (control_wait 1) ; Branch 1 + (control_wait 2)) ; Branch 2 + ;; After branch 1 and 2 join + (control_wait 11)) + + ;; Branch 3 + ((jump-point "loop-start") + (control_wait 3) ; Branch 3 + (control_wait 3.5) + (jump-to "loop-start") + ) + ))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/16_stepped_loop_inside_fork_reches_join.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/16_stepped_loop_inside_fork_reches_join.spec.ts new file mode 100644 index 00000000..d29d9e52 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/16_stepped_loop_inside_fork_reches_join.spec.ts @@ -0,0 +1,92 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + const branch3 = builder.add_op('control_wait', { id: 'branch3', + args: [ 3 ] + }); + + + const branch3_bot = builder.add_op('control_wait', { id: 'branch3-stepout', + args: [ 3.5 ] + }); + + const branch3_cond = builder.add_if(branch3_bot, branch3, { id: 'branch3-loop-helper', + cond: f => builder.add_stream('operator_equals', {args: [1, 1]}) + }); + + branch3.then_id(branch3_cond); + + builder.add_fork(trigger, [branch1, branch2, branch3 ]); + + // Join branches + const joiner = builder.add_trigger('trigger_when_all_completed', {args: [ + [ branch1, 'pulse' ], + [ branch2, 'pulse' ], + [ branch3_bot, 'pulse' ] + ]}); + joiner.then(f => f.add_op('control_wait', { id: 'joiner', args: [ 11 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-16: Stepped loop inside fork reaches join.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork + (control_wait 1) ; Branch 1 + (control_wait 2) ; Branch 2 + + ;; Branch 3 + ((jump-point "loop-start") + (control_wait 3) ; Branch 3 + (if (= 1 1) + () + ((jump-to "loop-start"))) + (control_wait 3.5)) + ) + (control_wait 11))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/17_if_else_simple_flow.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/17_if_else_simple_flow.spec.ts new file mode 100644 index 00000000..8997b3aa --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/17_if_else_simple_flow.spec.ts @@ -0,0 +1,73 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const stream_cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[stream_cond, 0]]}); + const root = trigger.then(f => f.add_op('control_wait', { args: [ 0 ] })) + const branch1_top = builder.add_op('control_wait', { args: [ 1 ] }); + const branch1_bot = branch1_top.then(f => f.add_op('control_wait', { args: [ 1.1 ]})); + + const branch2_top = builder.add_op('control_wait', { args: [ 2 ] }); + const branch2_bot = branch2_top.then(f => f.add_op('control_wait', { args: [ 2.1 ]})); + + const if_cond = builder.add_if(branch1_top, branch2_top, + { cond: f => f.add_stream('operator_equals', {args: [1, 1]}) + }); + + root.then_id(if_cond); + + // Join branches + const joiner = builder.add_trigger('trigger_when_first_completed', {args: [ + [ branch1_bot, 'pulse' ], + [ branch2_bot, 'pulse' ], + ]}); + joiner.then(f => f.add_op('control_wait', { id: 'joiner', args: [ 3 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-17: Simple If-Else flow.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((wait-seconds 0) + (if (= 1 1) + ((wait-seconds 1) + (wait-seconds 1.1)) + ((wait-seconds 2) + (wait-seconds 2.1))) + (control_wait 3))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/18_fork_branch_merged_twice.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/18_fork_branch_merged_twice.spec.ts new file mode 100644 index 00000000..19092b3c --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/18_fork_branch_merged_twice.spec.ts @@ -0,0 +1,96 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { lift_common_ops } from '../../../flow-editor/graph_transformations'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function process_flow(graph: FlowGraph): FlowGraph { + // Used by visualizing script to produce a processed version specific for this test + return lift_common_ops(graph); +} + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + const branch3 = builder.add_op('control_wait', { id: 'branch3', + args: [ 3 ] }); + + builder.add_fork(trigger, [branch1, branch2, branch3 ]); + + const branch4 = builder.add_op('control_wait', { id: 'branch4', + args: [ 2.4 ] + }) + + const branch5 = builder.add_op('control_wait', { id: 'branch5', + args: [ 2.5 ] + }) + + builder.add_fork(branch2, [branch4, branch5]); + + // Join branch 1 and 2 + const joiner12 = builder.add_trigger('trigger_when_all_completed', { id: 'joiner12', + args: [[ branch1, 'pulse' ], [branch4, 'pulse']]}) + .then(f => f.add_op('control_wait', { id: 'branch12', args: [ 12 ] })); + + // Join branch 2 and 3 + const joiner23 = builder.add_trigger('trigger_when_all_completed', { id: 'joiner23', + args: [[ branch5, 'pulse' ], [branch3, 'pulse']]}) + .then(f => f.add_op('control_wait', { id: 'branch23', args: [ 23 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-18: Fork branch merged twice.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ;; Operation 2 is lifted out of common joins + ((wait-seconds 2) + (fork + ((fork + ((wait-seconds 1)) + ((wait-seconds 2.4))) + (wait-seconds 12)) + ((fork + (wait-seconds 3) + (wait-seconds 2.5)) + (wait-seconds 23)) + ))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/19_nested_forks.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/19_nested_forks.spec.ts new file mode 100644 index 00000000..e58e5b27 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/19_nested_forks.spec.ts @@ -0,0 +1,102 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; +import { lift_common_ops } from '../../../flow-editor/graph_transformations'; + +export function process_flow(graph: FlowGraph): FlowGraph { + // Used by visualizing script to produce a processed version specific for this test + return lift_common_ops(graph); +} + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + builder.add_fork(trigger, [branch1, branch2 ]); + + const branch3 = builder.add_op('control_wait', { args: [ 3 ] }); + const branch4 = builder.add_op('control_wait', { args: [ 4 ] }); + + builder.add_fork(branch2, [branch3, branch4 ]); + + const branch5 = builder.add_op('control_wait', { args: [ 5 ] }); + const branch6 = builder.add_op('control_wait', { args: [ 6 ] }); + + builder.add_fork(branch4, [branch5, branch6 ]); + + + // Join branch 1 and 6 + const joiner16 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch1, 'pulse' ], [branch6, 'pulse']]}) + .then(f => f.add_op('control_wait', { args: [ 16 ] })); + + // Join branch 3 and 5 + const joiner35 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch3, 'pulse' ], [branch5, 'pulse']]}) + .then(f => f.add_op('control_wait', { args: [ 35 ] })); + + const joiner_all = builder.add_trigger('trigger_when_all_completed', {args: [[ joiner16, 'pulse' ], + [ joiner35, 'pulse' ] + ]}); + joiner_all.then(f => f.add_op('control_wait', { args: [ 9 ]})) + + const graph = builder.build(); + return graph; +} + +describe('Flow-19: Nested forks.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ;; Operations 2 and 4 are lifted out of common joins + ((wait-seconds 2) + (wait-seconds 4) + (fork + ;; Branch 1 & 6 + ((fork + ((wait-seconds 1)) + ((wait-seconds 6))) + (wait-seconds 16)) + ;; Branch 2 + ((fork + ((wait-seconds 5)) + ((wait-seconds 3))) + (wait-seconds 35))) + (wait-seconds 9) + )) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/20_fork_with_non_consecutive_output.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/20_fork_with_non_consecutive_output.spec.ts new file mode 100644 index 00000000..7c769c1b --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/20_fork_with_non_consecutive_output.spec.ts @@ -0,0 +1,66 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + const branch3 = builder.add_op('control_wait', { id: 'branch3', + args: [ 3 ] + }); + + builder.add_fork(trigger, [null, branch2, branch3 ]); + + // Join branch 2 and 3 + const joiner23 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch2, 'pulse' ], [branch3, 'pulse']]}); + joiner23.then(f => f.add_op('control_wait', { id: 'joiner23', args: [ 23 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-20: Fork with non consecutive output.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork + ((wait-seconds 2)) + + ((wait-seconds 3))) + (wait-seconds 23))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_01_fork_with_if_closer.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_01_fork_with_if_closer.spec.ts new file mode 100644 index 00000000..f06eb77d --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_01_fork_with_if_closer.spec.ts @@ -0,0 +1,63 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + builder.add_fork(trigger, [branch1, branch2]); + + // Join branch 1 and 2 + const joiner12 = builder.add_trigger('trigger_when_first_completed', {args: [[branch1, 'pulse'], [ branch2, 'pulse' ]]}); + joiner12.then(f => f.add_op('control_wait', { id: 'joiner12', args: [ 12 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-01: Fork with IF closer (when FIRST completed).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork :exit-when-first-completed + ((wait-seconds 1)) + ((wait-seconds 2))) + (wait-seconds 12))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_02_if_with_fork_closer.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_02_if_with_fork_closer.spec.ts new file mode 100644 index 00000000..46074362 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_02_if_with_fork_closer.spec.ts @@ -0,0 +1,39 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + trigger.then_id(builder.add_if(branch1, branch2, { + cond: [ cond, 0 ] + })) + + // Join branch 1 and 2 + const joiner12 = builder.add_trigger('trigger_when_all_completed', {args: [[branch1, 'pulse'], [ branch2, 'pulse' ]]}); + joiner12.then(f => f.add_op('control_wait', { id: 'joiner12', args: [ 12 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-02: If with fork closer (when ALL completed).', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*can get to Join.*with no fork.*/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_03_fork_if_wait_all_success.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_03_fork_if_wait_all_success.spec.ts new file mode 100644 index 00000000..98418e64 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_03_fork_if_wait_all_success.spec.ts @@ -0,0 +1,75 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', { id: 'eq-check', args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + builder.add_fork(trigger, [branch1, branch2]); + + // Join branch 1 and 2 + const if_true = builder.add_op('control_wait', { args: [ 3 ] }); + const if_false = builder.add_op('control_wait', { args: [ 4 ] }); + + branch2.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + + const if_joiner = builder.add_trigger('trigger_when_first_completed', {args: [[ if_true, 'pulse' ], [if_false, 'pulse']]}); + const joiner = builder.add_trigger('trigger_when_all_completed', {args: [[branch1, 'pulse'], [ if_joiner, 'pulse']]}); + joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-03: Fork then IF, close merging ALL (correct form).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork + ((wait-seconds 1)) + ((wait-seconds 2) + (if (flow-last-value "eq-check" 0) + ((wait-seconds 3)) + ((wait-seconds 4))) + )) + (wait-seconds 9))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_04_fork_if_wait_all_fail_no_if_merge.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_04_fork_if_wait_all_fail_no_if_merge.spec.ts new file mode 100644 index 00000000..3da091f5 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_04_fork_if_wait_all_fail_no_if_merge.spec.ts @@ -0,0 +1,49 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + builder.add_fork(trigger, [branch1, branch2]); + + // Join branch 1 and 2 + const if_true = builder.add_op('control_wait', { args: [ 3 ] }); + const if_false = builder.add_op('control_wait', { args: [ 4 ] }); + + branch2.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + + const joiner = builder.add_trigger('trigger_when_all_completed', {args: [[branch1, 'pulse'], [ if_true, 'pulse' ], [if_false, 'pulse']]}); + joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-04: Fork then IF, close merging ALL (fail: no if merge).', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*single conditional block.*has two connections to a fork join block/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_05_fork_if_wait_first_success.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_05_fork_if_wait_first_success.spec.ts new file mode 100644 index 00000000..0370eef9 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_05_fork_if_wait_first_success.spec.ts @@ -0,0 +1,75 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', { id: 'eq-check', args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + builder.add_fork(trigger, [branch1, branch2]); + + // Join branch 1 and 2 + const if_true = builder.add_op('control_wait', { args: [ 3 ] }); + const if_false = builder.add_op('control_wait', { args: [ 4 ] }); + + branch2.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + + const if_joiner = builder.add_trigger('trigger_when_first_completed', {args: [[ if_true, 'pulse' ], [if_false, 'pulse']]}); + const joiner = builder.add_trigger('trigger_when_first_completed', {args: [[branch1, 'pulse'], [ if_joiner, 'pulse']]}); + joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-05: Fork then IF, close merging FIRST (long form).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork :exit-when-first-completed + ((wait-seconds 1)) + ((wait-seconds 2) + (if (flow-last-value "eq-check" 0) + ((wait-seconds 3)) + ((wait-seconds 4))) + )) + (wait-seconds 9))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_06_fork_if_wait_first_success_shorter.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_06_fork_if_wait_first_success_shorter.spec.ts new file mode 100644 index 00000000..329ffc96 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_06_fork_if_wait_first_success_shorter.spec.ts @@ -0,0 +1,73 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {id: 'eq-check', args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const branch1 = builder.add_op('control_wait', { id: 'branch1', + args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { id: 'branch2', + args: [ 2 ] + }); + + builder.add_fork(trigger, [branch1, branch2]); + + // Join branch 1 and 2 + const if_true = builder.add_op('control_wait', { args: [ 3 ] }); + const if_false = builder.add_op('control_wait', { args: [ 4 ] }); + + branch2.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + const joiner = builder.add_trigger('trigger_when_first_completed', {args: [[branch1, 'pulse'], [ if_true, 'pulse'], [if_false, 'pulse']]}); + joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-06: Fork then IF, close merging first (short form).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((fork :exit-when-first-completed + ((wait-seconds 1)) + ((wait-seconds 2) + (if (flow-last-value "eq-check" 0) + ((wait-seconds 3)) + ((wait-seconds 4))) + )) + (wait-seconds 9))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_07_if_fork_first_long.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_07_if_fork_first_long.spec.ts new file mode 100644 index 00000000..10c42a2c --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_07_if_fork_first_long.spec.ts @@ -0,0 +1,74 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {id: 'eq-check', args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const if_true = builder.add_op('control_wait', { args: [ 1 ] }); + const if_false = builder.add_op('control_wait', { args: [ 2 ] }); + trigger.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + const branch3 = builder.add_op('control_wait', { args: [ 3 ] + }); + + const branch4 = builder.add_op('control_wait', { id: 'branch2', + args: [ 4 ] + }); + + builder.add_fork(if_true, [branch3, branch4]); + + // Join branch 1 and 2 + + + const fork_joiner = builder.add_trigger('trigger_when_first_completed', {args: [[branch3, 'pulse'], [ branch4, 'pulse']]}); + + const if_joiner = builder.add_trigger('trigger_when_first_completed', {args: [[fork_joiner, 'pulse'], [ if_false, 'pulse']]}); + if_joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-07: IF then fork, close with IF merging (wait FIRST, long form).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((if (flow-last-value "eq-check" 0) + ((wait-seconds 1) + (fork :exit-when-first-completed + ((wait-seconds 3)) + ((wait-seconds 4)))) + ((wait-seconds 2))) + (wait-seconds 9))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_08_if_fork_first_short.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_08_if_fork_first_short.spec.ts new file mode 100644 index 00000000..7f6735a4 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_08_if_fork_first_short.spec.ts @@ -0,0 +1,70 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {id: 'eq-check', args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const if_true = builder.add_op('control_wait', { args: [ 1 ] }); + const if_false = builder.add_op('control_wait', { args: [ 2 ] }); + trigger.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + const branch3 = builder.add_op('control_wait', { args: [ 3 ] + }); + + const branch4 = builder.add_op('control_wait', { id: 'branch2', + args: [ 4 ] + }); + + builder.add_fork(if_true, [branch3, branch4]); + + // Join branch 1 and 2 + const if_joiner = builder.add_trigger('trigger_when_first_completed', {args: [[branch3, 'pulse'], [branch4, 'pulse'], [ if_false, 'pulse']]}); + if_joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-08: IF then fork, close with IF merging (wait FIRST, short form).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((if (flow-last-value "eq-check" 0) + ((wait-seconds 1) + (fork :exit-when-first-completed + ((wait-seconds 3)) + ((wait-seconds 4)))) + ((wait-seconds 2))) + (wait-seconds 9))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_09_if_fork_wait_all.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_09_if_fork_wait_all.spec.ts new file mode 100644 index 00000000..bfda8228 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_09_if_fork_wait_all.spec.ts @@ -0,0 +1,72 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', { id: 'eq-check', args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const if_true = builder.add_op('control_wait', { args: [ 1 ] }); + const if_false = builder.add_op('control_wait', { args: [ 2 ] }); + trigger.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + const branch3 = builder.add_op('control_wait', { args: [ 3 ] + }); + + const branch4 = builder.add_op('control_wait', { id: 'branch2', + args: [ 4 ] + }); + + + builder.add_fork(if_true, [branch3, branch4]); + + // Join branch 1 and 2 + const fork_joiner = builder.add_trigger('trigger_when_all_completed', {args: [[branch3, 'pulse'], [ branch4, 'pulse']]}); + const if_joiner = builder.add_trigger('trigger_when_first_completed', {args: [[ fork_joiner, 'pulse' ], [if_false, 'pulse']]}); + if_joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-09: IF then Fork, close merging ALL (correct form).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11) + ) + ((if (flow-last-value "eq-check" 0) + ((wait-seconds 1) + (fork + ((wait-seconds 3)) + ((wait-seconds 4)))) + ((wait-seconds 2))) + (wait-seconds 9))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/21_10_if_fork_wait_all_fail.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/21_10_if_fork_wait_all_fail.spec.ts new file mode 100644 index 00000000..3014d36e --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/21_10_if_fork_wait_all_fail.spec.ts @@ -0,0 +1,48 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { are_equivalent_ast } from './utils.spec'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const if_true = builder.add_op('control_wait', { args: [ 1 ] }); + const if_false = builder.add_op('control_wait', { args: [ 2 ] }); + trigger.then_id(builder.add_if(if_true, if_false, { cond: [cond, 0] })) + + const branch3 = builder.add_op('control_wait', { args: [ 3 ] + }); + + const branch4 = builder.add_op('control_wait', { id: 'branch2', + args: [ 4 ] + }); + + + builder.add_fork(if_true, [branch3, branch4]); + + // Join branch 1 and 2 + const fork_joiner = builder.add_trigger('trigger_when_all_completed', {args: [[branch3, 'pulse'], [ branch4, 'pulse']]}); + const if_joiner = builder.add_trigger('trigger_when_all_completed', {args: [[ fork_joiner, 'pulse' ], [if_false, 'pulse']]}); + if_joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-21-10: IF then Fork, close merging ALL (wrong form).', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*can get to Join.*with no fork.*$/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/22_stepped_loop_inside_fork_goes_outside.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/22_stepped_loop_inside_fork_goes_outside.spec.ts new file mode 100644 index 00000000..b7315c4b --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/22_stepped_loop_inside_fork_goes_outside.spec.ts @@ -0,0 +1,44 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const before_fork = builder.add_op('control_wait', { args: [ 0 ] + }); + + const branch1 = builder.add_op('control_wait', { args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { args: [ 2 ] + }); + + const branch3 = builder.add_op('control_wait', { args: [ 3 ] + }); + + trigger.then(before_fork); + builder.add_fork(before_fork, [branch1, branch2, branch3 ]); + + branch3.then(before_fork); + + // Join branch 1 and 2 + const joiner12 = builder.add_trigger('trigger_when_all_completed', {args: [[ branch1, 'pulse' ], [branch2, 'pulse']]}); + joiner12.then(f => f.add_op('control_wait', { id: 'joiner12', args: [ 11 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-22: Stepped loop inside fork goes outside of it.', () => { + it('Validation should pass', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Loop around Fork blocks not allowed.*/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/23_calling_from_second_source_to_forked_flow_fails.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/23_calling_from_second_source_to_forked_flow_fails.spec.ts new file mode 100644 index 00000000..bb909521 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/23_calling_from_second_source_to_forked_flow_fails.spec.ts @@ -0,0 +1,38 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source1 = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + const source2 = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + + const trigger1 = builder.add_trigger('trigger_on_signal', {args: [[source1, 0]]}); + const trigger2 = builder.add_trigger('trigger_on_signal', {args: [[source2, 0]]}); + + // Stepped section + const branch1 = builder.add_op('control_wait', { args: [ 1 ] + }); + + const branch2 = builder.add_op('control_wait', { args: [ 2 ] + }); + + builder.add_fork(trigger1, [branch1, branch2]); + trigger2.then(branch2); + + // Join branch 1 and 2 + const joiner = builder.add_trigger('trigger_when_all_completed', {args: [[ branch1, 'pulse' ], [branch2, 'pulse']]}); + joiner.then(f => f.add_op('control_wait', { args: [ 9 ] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-23: Calling from second source to forked flow fails.', () => { + it('Validation should pass', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError: Block .* can get to Join .* with no fork.*/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/24_get_output_of_operation.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/24_get_output_of_operation.spec.ts new file mode 100644 index 00000000..3cc27cc7 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/24_get_output_of_operation.spec.ts @@ -0,0 +1,55 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: options.source_id, message: 'UTC time'}); + const trigger = builder.add_trigger('trigger_on_signal', {args: [[source, 0]]}); + + // Stepped section + const operation = builder.add_op('create_issue', { id: 'operation-with-value', + namespace: 'gitlab', + args: [ "Sample project", "Sample title" ] + }); + trigger.then(operation); + operation + .then(f => f.add_op('control_wait', { args: [ 1 ] })) + .then(f => f.add_op('logging_add_log', { args: [[operation, 1]] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-24: Get output of operation block.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const TIME_BLOCK = "ad97e5d1-c725-4cc6-826f-30057f239635"; + const OP_BLOCK_ID = "operation-with-value"; + + are_equivalent_ast(compile(gen_flow({ source_id: TIME_BLOCK })), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (call-service id: "gitlab" + action: "create_issue" + values: ("Sample project" "Sample title")) + (wait-seconds 1) + (log (flow-last-value "${OP_BLOCK_ID}" 1)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/25_01_defend_against_glitches.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/25_01_defend_against_glitches.spec.ts new file mode 100644 index 00000000..a9f6138a --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/25_01_defend_against_glitches.spec.ts @@ -0,0 +1,54 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: options.source_id, message: 'UTC time'}); + const trigger = builder.add_trigger('trigger_on_signal', {args: [[source, 0]]}); + + // Var + const xblock = builder.add_variable_getter_node('x', { id: 'x1' }); + const xblock2 = builder.add_variable_getter_node('x', { id: 'x2' }); + + const addition1 = builder.add_op('operator_add', { id: 'add1', args: [[xblock, 0], 1] }); + const addition2 = builder.add_op('operator_add', { id: 'add2', args: [[addition1, 0], [xblock, 0]] }); + const log1 = builder.add_op('logging_add_log', { id: 'log1', args: [[addition1, 0]] }); + const log2 = builder.add_op('logging_add_log', { id: 'log2', args: [[addition2, 0]] }); + const log3 = builder.add_op('logging_add_log', { id: 'log3', args: [[xblock2, 0]] }); + + trigger.then(log1).then(log2).then(log3); + + const graph = builder.build(); + return graph; +} + +describe('Flow-25-01: Defend against glitches.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const TIME_BLOCK = "ad97e5d1-c725-4cc6-826f-30057f239635"; + + are_equivalent_ast(compile(gen_flow({ source_id: TIME_BLOCK })), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (log (+ (get-var x) 1)) + (log (+ (flow-last-value "add1" 0) (flow-last-value "x1" 0))) + (log (get-var x)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/25_02_same_getter_used_twice_by_same_operation.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/25_02_same_getter_used_twice_by_same_operation.spec.ts new file mode 100644 index 00000000..8919375f --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/25_02_same_getter_used_twice_by_same_operation.spec.ts @@ -0,0 +1,58 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { extract_internally_reused_arguments } from '../../../flow-editor/graph_transformations'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function process_flow(graph: FlowGraph): FlowGraph { + // Used by visualizing script to produce a processed version specific for this test + return extract_internally_reused_arguments(graph); +} + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: options.source_id, message: 'UTC time'}); + const trigger = builder.add_trigger('trigger_on_signal', {args: [[source, 0]]}); + + // Var + const xblock = builder.add_variable_getter_node('x', { id: 'x' }); + + const addition1 = builder.add_op('operator_add', { id: 'add1', args: [[xblock, 0], 1] }); + const addition2 = builder.add_op('operator_add', { id: 'add2', args: [[xblock, 0], 2] }); + const join_add = builder.add_op('operator_add', { id: 'join_add', args: [[addition1, 0], [addition2, 0]] }); + const log1 = builder.add_op('logging_add_log', { id: 'log1', args: [[join_add, 0]] }); + + trigger.then(log1); + + const graph = builder.build(); + return graph; +} + +describe('Flow-25-02: Same getter used twice by same operation.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const TIME_BLOCK = "ad97e5d1-c725-4cc6-826f-30057f239635"; + + are_equivalent_ast(compile(gen_flow({ source_id: TIME_BLOCK })), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (preload (get-var x)) + (log (+ (+ (flow-last-value x 0) 1) + (+ (flow-last-value x 0) 2))) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/25_03_same_getter_twice_two_asts.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/25_03_same_getter_twice_two_asts.spec.ts new file mode 100644 index 00000000..744b97ac --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/25_03_same_getter_twice_two_asts.spec.ts @@ -0,0 +1,64 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { extract_internally_reused_arguments } from '../../../flow-editor/graph_transformations'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function process_flow(graph: FlowGraph): FlowGraph { + // Used by visualizing script to produce a processed version specific for this test + return extract_internally_reused_arguments(graph); +} + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: options.source_id, message: 'UTC time'}); + const trigger1 = builder.add_trigger('trigger_on_signal', {args: [[source, 0]]}); + const trigger2 = builder.add_trigger('trigger_on_signal', {args: [[source, 0]]}); + + // Var + const xblock = builder.add_variable_getter_node('x', { id: 'x' }); + + const addition11 = builder.add_op('operator_add', { id: 'add11', args: [[xblock, 0], 1] }); + const addition12 = builder.add_op('operator_add', { id: 'add12', args: [[xblock, 0], 2] }); + const join_add1 = builder.add_op('operator_add', { id: 'join_add1', args: [[addition11, 0], [addition12, 0]] }); + const log1 = builder.add_op('logging_add_log', { id: 'log1', args: [[join_add1, 0]] }); + + const addition21 = builder.add_op('operator_add', { id: 'add21', args: [[xblock, 0], 1] }); + const addition22 = builder.add_op('operator_add', { id: 'add22', args: [[xblock, 0], 2] }); + const join_add2 = builder.add_op('operator_add', { id: 'join_add2', args: [[addition21, 0], [addition22, 0]] }); + const log2 = builder.add_op('logging_add_log', { id: 'log2', args: [[join_add2, 0]] }); + + trigger1.then(log1); + trigger2.then(log2); + + const graph = builder.build(); + return graph; +} + +describe('Flow-25-03: Same getter used twice in two ASTs.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const TIME_BLOCK = "ad97e5d1-c725-4cc6-826f-30057f239635"; + + const expected_ast = gen_compiled(dsl_to_ast(`;PM-DSL;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (preload (get-var x)) + (log (+ (+ (flow-last-value x 0) 1) + (+ (flow-last-value x 0) 2))) + `)); + are_equivalent_ast(compile(gen_flow({ source_id: TIME_BLOCK })), + [ expected_ast, expected_ast ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/26_trigger_with_no_getter.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/26_trigger_with_no_getter.spec.ts new file mode 100644 index 00000000..4466263f --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/26_trigger_with_no_getter.spec.ts @@ -0,0 +1,52 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile, get_unreachable } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Stepped section + const trigger = builder.add_trigger('on_new_message', {id: 'trigger', namespace: chat, args: []}); + const operation = builder.add_op('respond', { namespace: chat, + args: [[ trigger, 1]] + }); + trigger.then(operation); + + const graph = builder.build(); + return graph; +} + +describe('Flow-26: Trigger with no getter.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should find no unreachable blocks', async () => { + expect(get_unreachable(gen_flow())).toEqual([]); + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.de5baefb-13da-457e-90a5-57a753da8891.on_new_message ) + (call-service id: ${CHAT_SVC} + action: respond + values: ((flow-last-value trigger 1) + )) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/27_01_control_wait_for_value_directly_bridge_trigger.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/27_01_control_wait_for_value_directly_bridge_trigger.spec.ts new file mode 100644 index 00000000..b79a2c5e --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/27_01_control_wait_for_value_directly_bridge_trigger.spec.ts @@ -0,0 +1,46 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Stepped section + const trigger = builder.add_trigger('on_new_message', {id: 'trigger', namespace: chat, args: []}); + const waited = builder.add_trigger('on_new_message', {id: 'waited', namespace: chat, args: []}); + + const operation = builder.add_op('control_wait_for_next_value', { args: [[ waited, 1]] + }); + trigger.then(operation); + + const graph = builder.build(); + return graph; +} + +describe('Flow-27-01: Wait for value might have direct connection from a bridge trigger.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.de5baefb-13da-457e-90a5-57a753da8891.on_new_message ) + (control_wait_for_next_value (flow-last-value waited 1)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/27_02_control_wait_for_value_directly_variable.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/27_02_control_wait_for_value_directly_variable.spec.ts new file mode 100644 index 00000000..da4051b0 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/27_02_control_wait_for_value_directly_variable.spec.ts @@ -0,0 +1,45 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Stepped section + const trigger = builder.add_trigger('on_new_message', {id: 'trigger', namespace: chat, args: []}); + + const operation = builder.add_op('control_wait_for_next_value', { args: [{from_variable: 'reference'}] + }); + trigger.then(operation); + + const graph = builder.build(); + return graph; +} + +describe('Flow-27-02: Wait for value might have direct connection from a variable.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const CHAT_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.de5baefb-13da-457e-90a5-57a753da8891.on_new_message ) + (control_wait_for_next_value (get-var reference)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/27_03_control_wait_for_value_requires_connection.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/27_03_control_wait_for_value_requires_connection.spec.ts new file mode 100644 index 00000000..a5ae4c13 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/27_03_control_wait_for_value_requires_connection.spec.ts @@ -0,0 +1,29 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Stepped section + const trigger = builder.add_trigger('on_new_message', {id: 'trigger', namespace: chat, args: []}); + + const operation = builder.add_op('control_wait_for_next_value', { args: [] + }); + trigger.then(operation); + + const graph = builder.build(); + return graph; +} + +describe('Flow-27-03: Wait for value requires connection.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Required input has no connections.*/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/27_04_control_wait_for_value_not_accept_normal_getter.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/27_04_control_wait_for_value_not_accept_normal_getter.spec.ts new file mode 100644 index 00000000..2c03e272 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/27_04_control_wait_for_value_not_accept_normal_getter.spec.ts @@ -0,0 +1,30 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Stepped section + const trigger = builder.add_trigger('on_new_message', {id: 'trigger', namespace: chat, args: []}); + const test = builder.add_stream('operator_equals', {args: [0, 0]}); + + const operation = builder.add_op('control_wait_for_next_value', { args: [[test, 0]] + }); + trigger.then(operation); + + const graph = builder.build(); + return graph; +} + +describe('Flow-27-04: Wait for value does not accept normal getter.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Wait for value does not accept a getter as input.*/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/27_05_control_wait_for_value_not_accept_constant.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/27_05_control_wait_for_value_not_accept_constant.spec.ts new file mode 100644 index 00000000..ef3540d3 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/27_05_control_wait_for_value_not_accept_constant.spec.ts @@ -0,0 +1,29 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { validate } from '../../../flow-editor/graph_validation'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const chat = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Stepped section + const trigger = builder.add_trigger('on_new_message', {id: 'trigger', namespace: chat, args: []}); + + const operation = builder.add_op('control_wait_for_next_value', { args: ["sample"] + }); + trigger.then(operation); + + const graph = builder.build(); + return graph; +} + +describe('Flow-27-05: Wait for value does not accept a constant input.', () => { + it('Validation should FAIL', async () => { + expect(() => validate(gen_flow())) + .toThrowError(/^ValidationError:.*Wait for value does not accept a constant as input.*/i) + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/27_06_control_wait_for_value_accept_time.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/27_06_control_wait_for_value_accept_time.spec.ts new file mode 100644 index 00000000..84d53dcd --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/27_06_control_wait_for_value_accept_time.spec.ts @@ -0,0 +1,49 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + // Stepped section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + + const cond = builder.add_stream('operator_equals', {args: [[source, 0], 11]}); + const trigger = builder.add_trigger('trigger_when_all_true', {id: 'trigger', args: [[cond, 0]]}) + + const waited = builder.add_stream('flow_utc_time', {id: 'intermediate', message: 'UTC time'}); + + const operation = builder.add_op('control_wait_for_next_value', { args: [[ waited, 1]] + }); + trigger.then(operation); + + const graph = builder.build(); + return graph; +} + +describe('Flow-27-06: Wait for value might have direct connection from a time trigger.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (flow-last-value "source" 0) + 11)) + ((control_wait_for_next_value (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}" )))) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/28_filter_on_trigger.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/28_filter_on_trigger.spec.ts new file mode 100644 index 00000000..191ae2e9 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/28_filter_on_trigger.spec.ts @@ -0,0 +1,55 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Services + const dataSource = builder.add_service('de5baefb-13da-457e-90a5-57a753da8891'); + + // Trigger + const trigger = builder.add_trigger('trigger_on_message', {id: 'trigger', args: [], namespace: dataSource}); + + // Condition that flows from trigger + const cond = builder.add_getter('operator_equals', {args: [[ trigger, 1], "/clear"]}); + const check = builder.add_op('control_if_else', {args: [[cond, 0]]}); + + // Check result + const log1 = builder.add_op('logging_add_log', { id: 'log1', args: ["Clearing"] }); + const log2 = builder.add_op('logging_add_log', { id: 'log2', args: ["Not clearing"] }); + + trigger.then(check); + check.then(log1, 0); + check.then(log2, 1); + + const graph = builder.build(); + return graph; +} + +describe('Flow-28: Filter trigger result.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const DATA_SVC = "de5baefb-13da-457e-90a5-57a753da8891"; + + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.${DATA_SVC}.trigger_on_message) + (if (= (flow-last-value "trigger" 1) "/clear") + ((log "Clearing")) + ((log "Not clearing"))) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/29_trigger_with_button.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/29_trigger_with_button.spec.ts new file mode 100644 index 00000000..705dbba4 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/29_trigger_with_button.spec.ts @@ -0,0 +1,41 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Trigger + const trigger = builder.add_trigger('simple_button', {id: 'trigger', args: []}); + + // Simple operation + const log1 = builder.add_op('logging_add_log', { id: 'log1', args: ["Triggered"] }); + + trigger.then(log1); + + const graph = builder.build(); + return graph; +} + +describe('Flow-29: Trigger with button.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger) + (log "Triggered") + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/30_update_output_from_variable.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/30_update_output_from_variable.spec.ts new file mode 100644 index 00000000..23394794 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/30_update_output_from_variable.spec.ts @@ -0,0 +1,35 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + builder.add_getter('dynamic_text', { id: 'out', args: [{from_variable: 'x'}] }); + + const graph = builder.build(); + return graph; +} + +describe('Flow-30: Update output from variable.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (on-var x) + (services.ui.dynamic_text.out (get-var x)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/31_stepped_out_to_flow.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/31_stepped_out_to_flow.spec.ts new file mode 100644 index 00000000..6f47e63c --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/31_stepped_out_to_flow.spec.ts @@ -0,0 +1,63 @@ +import { split_streaming_after_stepped } from '../../../flow-editor/graph_transformations'; +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function process_flow(graph: FlowGraph): FlowGraph { + // Used by visualizing script to produce a processed version specific for this test + return split_streaming_after_stepped(graph); +} + +const SERVICE = 'placeholder'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Trigger + const trigger = builder.add_trigger('simple_button', {id: 'trigger', args: []}); + + // Simple operation + const operation = builder.add_op('op_with_output', { id: 'op', args: ["Triggered"], namespace: SERVICE }); + const out_of_tree_op = builder.add_op('logging_add_log', { id: 'log', args: ["Out of tree"] }); + + trigger + .then(operation) + .then(out_of_tree_op) + ; + + builder.add_getter('dynamic_text', { id: 'out', args: [[operation, 1]] }); + + const graph = builder.build(); + return graph; +} + +describe('Flow-31: Update streaming block from stepped flow.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger) + (call-service id: ${SERVICE} action: op_with_output values: ("Triggered")) + (log "Out of tree") + `)), + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (on-block-run "op" 1) + (services.ui.dynamic_text.out (flow-last-value "op" 1)) + ` + )) + + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/32_convergence_of_multiple_trigger_values.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/32_convergence_of_multiple_trigger_values.spec.ts new file mode 100644 index 00000000..4e9bc631 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/32_convergence_of_multiple_trigger_values.spec.ts @@ -0,0 +1,58 @@ +import { split_streaming_after_stepped } from '../../../flow-editor/graph_transformations'; +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +const SERVICE = 'placeholder'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Trigger + const trigger1 = builder.add_trigger('simple_button', {id: 'trigger1'}); + const trigger2 = builder.add_trigger('simple_button', {id: 'trigger2'}); + (builder.nodes.trigger1.value as any).extra.textContent = '1'; + (builder.nodes.trigger2.value as any).extra.textContent = '2'; + + // Simple operation + const out_of_tree_op = builder.add_op('logging_add_log', { id: 'log', args: [] }); + + trigger1.then(out_of_tree_op); + trigger2.then(out_of_tree_op); + + builder.establish_connection(['trigger1', 1], ['log', 1]); + builder.establish_connection(['trigger2', 1], ['log', 1]); + + const graph = builder.build(); + return graph; +} + +describe('Flow-32: Convergence of multiple trigger values.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger1) + (log (data_ui_block_value "trigger1")) + `)), + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger2) + (log (data_ui_block_value "trigger2")) + ` + )) + + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/33_parallel_trigger_to_flow_and_stepped.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/33_parallel_trigger_to_flow_and_stepped.spec.ts new file mode 100644 index 00000000..0e3c7791 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/33_parallel_trigger_to_flow_and_stepped.spec.ts @@ -0,0 +1,50 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +const SERVICE = 'placeholder'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Trigger + const trigger = builder.add_trigger('trigger_on_message', {id: 'trigger', args: [], namespace: SERVICE}); + + // Simple operation + trigger.then(b => b.add_op('logging_add_log', { id: 'log', args: [[trigger, 1]] })); + + // Streaming operation + builder.add_getter('dynamic_text', { id: 'out', args: [[trigger, 1 ]] }); + + const graph = builder.build(); + return graph; +} + +describe('Flow-33: Parallel trigger to flow and stepped.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.${SERVICE}.trigger_on_message) + (log (flow-last-value trigger 1)) + `)), + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.${SERVICE}.trigger_on_message) + (services.ui.dynamic_text.out (flow-last-value trigger 1)) + `)) + + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/34_show_date_on_dynamic_text.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/34_show_date_on_dynamic_text.spec.ts new file mode 100644 index 00000000..1e86875c --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/34_show_date_on_dynamic_text.spec.ts @@ -0,0 +1,58 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Trigger + const trigger = builder.add_trigger('flow_utc_date', {id: 'trigger', args: []}); + + // Streaming operation + builder.add_getter('dynamic_text', { id: 'year', args: [[trigger, 0 ]] }); + builder.add_getter('dynamic_text', { id: 'month', args: [[trigger, 1 ]] }); + builder.add_getter('dynamic_text', { id: 'day', args: [[trigger, 2 ]] }); + builder.add_getter('dynamic_text', { id: 'dow', args: [[trigger, 3 ]] }); + + const graph = builder.build(); + return graph; +} + +describe('Flow-34: Show date on dynamic text.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + const TIME_SVC = '0093325b-373f-4f1c-bace-4532cce79df4'; + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_date from_service: ${TIME_SVC}) + (services.ui.dynamic_text.year (flow-last-value trigger 0)) + `)), + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_date from_service: ${TIME_SVC}) + (services.ui.dynamic_text.month (flow-last-value trigger 1)) + `)), + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_date from_service: ${TIME_SVC}) + (services.ui.dynamic_text.day (flow-last-value trigger 2)) + `)), + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_date from_service: ${TIME_SVC}) + (services.ui.dynamic_text.dow (flow-last-value trigger 3)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/35_value_from_textbox.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/35_value_from_textbox.spec.ts new file mode 100644 index 00000000..2379e1ec --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/35_value_from_textbox.spec.ts @@ -0,0 +1,40 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Trigger + const trigger = builder.add_trigger('simple_button', {id: 'trigger', args: []}); + const textbox = builder.add_trigger('text_box', {id: 'textbox', args: []}); + + // Simple operation + trigger.then(b => b.add_op('logging_add_log', { id: 'log1', args: [[textbox, 1]] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-35: Value from textbox.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger) + (log (data_ui_block_value textbox)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/36_if_else_in_loop.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/36_if_else_in_loop.spec.ts new file mode 100644 index 00000000..9ec6571c --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/36_if_else_in_loop.spec.ts @@ -0,0 +1,54 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Trigger + const trigger = builder.add_trigger('simple_button', {id: 'trigger', args: []}); + + // Condition & Check + const cond = builder.add_stream('operator_equals', { id: 'equals', args: [1, 2, 11]}); + const check = builder.add_op('control_if_else', { id: 'check', args: [ [cond, 0] ] }); + + // /----------------\ + // ↓ | + // Trigger → wait 1 -/ + // | + // \--→ wait 2 (finish) + // + trigger.then(check).then(f => f.add_op('control_wait', { id: 'true-branch', args: [ 1 ]})).then(check); + check.then(f => f.add_op('control_wait', { id: 'false-branch', args: [ 2 ] }), 1); + + const graph = builder.build(); + return graph; +} + +describe('Flow-36: If-else in loop.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger) + (jump-point "check") + (if (= 1 2 11) + ((wait-seconds 1) + (jump-to "check") + ) + ((wait-seconds 2))) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/37_repeat_loop.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/37_repeat_loop.spec.ts new file mode 100644 index 00000000..19a54fdf --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/37_repeat_loop.spec.ts @@ -0,0 +1,72 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(options?: { two_ops: boolean }): FlowGraph { + if (!options) { options = { two_ops: false }; } + + const builder = new GraphBuilder(); + + // Trigger + const trigger = builder.add_trigger('simple_button', {id: 'trigger', args: []}); + + // Loop + const loop = builder.add_op('control_repeat', { id: 'loop', args: [ 3 ] }); + + // + // Loop → wait 1 (iteration completed) + // | + // \→ wait 2 (finish) + // + + const log = builder.add_op('logging_add_log', { id: 'in-loop', args: [ [loop, 1] ]}); + + trigger + .then(loop) + .then(log) + + if (options.two_ops) { + log.then(f => f.add_op('control_wait', { args: [ 1 ] })); + } + + loop.then(f => f.add_op('logging_add_log', { id: 'out-of-loop', args: [ [loop, 1] ] }), 2); + + const graph = builder.build(); + return graph; +} + +describe('Flow-37: Repeat loop.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile with one op', async () => { + are_equivalent_ast(compile(gen_flow({ two_ops: false })), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger) + (repeat 3 + (log (flow-last-value "loop" 1))) + (log (flow-last-value "loop" 1)) + `)) + ]); + }); + + it('Should be able to compile with two ops', async () => { + are_equivalent_ast(compile(gen_flow({ two_ops: true })), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.trigger) + (repeat 3 + (log (flow-last-value "loop" 1)) + (wait-seconds 1)) + (log (flow-last-value "loop" 1)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/38_01_extract_wait_for_monitor_as_argument.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/38_01_extract_wait_for_monitor_as_argument.spec.ts new file mode 100644 index 00000000..ca021363 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/38_01_extract_wait_for_monitor_as_argument.spec.ts @@ -0,0 +1,43 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +const TIME_SVC = '0093325b-373f-4f1c-bace-4532cce79df4'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Source + const source = builder.add_trigger('flow_utc_date', {id: 'time', args: []}); + + // Stepper section operation + const trigger = builder.add_trigger('simple_button', {id: 'button', args: []}); + trigger.then(b => b.add_op('logging_add_log', { id: 'logger', args: [[source, 1 ]] })); + + const graph = builder.build(); + return graph; +} + +describe('Flow-38: Extract wait-for-monitor as argument.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.button) + (wait-for-monitor key: utc_date from_service: ${TIME_SVC}) + (log (flow-last-value time 1)) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/38_02_extract_wait_for_monitor_as_argument_in_contents.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/38_02_extract_wait_for_monitor_as_argument_in_contents.spec.ts new file mode 100644 index 00000000..fb37d90a --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/38_02_extract_wait_for_monitor_as_argument_in_contents.spec.ts @@ -0,0 +1,54 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +const TIME_SVC = '0093325b-373f-4f1c-bace-4532cce79df4'; + +export function gen_flow(options?: { source_id?: string }): FlowGraph { + if (!options) { options = {} } + + const builder = new GraphBuilder(); + + // Source + const source = builder.add_trigger('flow_utc_date', {id: 'time', args: []}); + + // Stepper section operation + const trigger = builder.add_trigger('simple_button', {id: 'button', args: []}); + + const branch1 = builder.add_op('logging_add_log', { id: 'logger-1', args: [[source, 1 ]] }); + const branch2 = builder.add_op('logging_add_log', { id: 'logger-2', args: [[source, 2 ]] }); + + const if_cond = builder.add_if(branch1, branch2, + { cond: f => f.add_stream('operator_equals', {args: [1, 1]}) + }); + + trigger.then_id(if_cond); + + const graph = builder.build(); + return graph; +} + +describe('Flow-38-02: Extract wait-for-monitor as argument (in contents).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + it('Should be able to compile', async () => { + are_equivalent_ast(compile(gen_flow()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.ui.simple_button.button) + (if (= 1 1) + ((wait-for-monitor key: utc_date from_service: ${TIME_SVC}) + (log (flow-last-value time 1))) + ((wait-for-monitor key: utc_date from_service: ${TIME_SVC}) + (log (flow-last-value time 2)))) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/90_reactive_get_even.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/90_reactive_get_even.spec.ts new file mode 100644 index 00000000..20decd68 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/90_reactive_get_even.spec.ts @@ -0,0 +1,53 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + + const mod = builder.add_stream('operator_modulo', {args: [[source, 0], 2]}); + const cond = builder.add_stream('operator_equals', {args: [[mod, 0], 0]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + const op = builder.add_op('logging_add_log', { args: [ [source, 0] ] + }); + trigger.then(op); + + const graph = builder.build(); + return graph; +} + +describe('Flow-90: [Reactive] Get even values.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (mod (flow-last-value "source" 0) 2) + 0) + ) + ((log (flow-last-value "source" 0)))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/91_1_reactive_take_n_first.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/91_1_reactive_take_n_first.spec.ts new file mode 100644 index 00000000..9a1092d7 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/91_1_reactive_take_n_first.spec.ts @@ -0,0 +1,66 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + + const mod = builder.add_stream('operator_modulo', {args: [[source, 0], 2]}); + const cond = builder.add_stream('operator_equals', {args: [[mod, 0], 0]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const update = builder.add_op('data_setvariableto', { + args: [ + [f => f.add_getter('operator_add', { args: [ 1, { from_variable: 'counter' } ] }) , 0] ], + slots: { 'variable': 'counter' } + }); + const op = builder.add_op('logging_add_log', { args: [ [source, 0] ] + }); + + const take_cond = builder.add_if(update, null, { + cond: [f => f.add_getter('operator_lt', { args: [ { from_variable: 'counter' }, 'N' ] }), 0] + }); + trigger.then_id(take_cond); + update.then(op); + + const graph = builder.build(); + return graph; +} + +describe('Flow-91-1: [Reactive] Take N first values.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (mod (flow-last-value "source" 0) 2) + 0) + ) + ((if (< (get-var counter) N) + ((set-var counter (+ 1 (get-var counter))) + (log (flow-last-value "source" 0)))))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/91_2_reactive_take_n_first_inverted_if.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/91_2_reactive_take_n_first_inverted_if.spec.ts new file mode 100644 index 00000000..dd1bd0d3 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/91_2_reactive_take_n_first_inverted_if.spec.ts @@ -0,0 +1,68 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + + const mod = builder.add_stream('operator_modulo', {args: [[source, 0], 2]}); + const cond = builder.add_stream('operator_equals', {args: [[mod, 0], 0]}); + + // Stepped section + const trigger = builder.add_trigger('trigger_when_all_true', {args: [[cond, 0]]}); + + const update = builder.add_op('data_setvariableto', { + args: [ + [f => f.add_getter('operator_add', { args: [ 1, { from_variable: 'counter' } ] }) , 0] ], + slots: { 'variable': 'counter' } + }); + const op = builder.add_op('logging_add_log', { args: [ [source, 0] ] + }); + + const take_cond = builder.add_if(null, update, { + cond: [f => f.add_getter('operator_gt', { args: [ { from_variable: 'counter' }, 'N' ] }), 0] + }); + trigger.then_id(take_cond); + update.then(op); + + const graph = builder.build(); + return graph; +} + +describe('Flow-91-2: [Reactive] Take N first values (inverted if).', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (and (= (mod (flow-last-value "source" 0) 2) + 0) + ) + ((if (> (get-var counter) N) + () + ; Else branch ↓ + ((set-var counter (+ 1 (get-var counter))) + (log (flow-last-value "source" 0)))))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/92_reactive_take_n_last.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/92_reactive_take_n_last.spec.ts new file mode 100644 index 00000000..cd878037 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/92_reactive_take_n_last.spec.ts @@ -0,0 +1,65 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + + // Stepped section + const trigger = builder.add_trigger('trigger_on_signal', {args: [[source, 0]]}); + + const make_space = builder.add_op('data_deleteoflist', { slots: { list: 'latest' }, args: [ 1 ] }) + + const add_new = builder.add_op('data_addtolist', { slots: { list: 'latest' }, args: [ [source, 0] ] }) + make_space.then(add_new); + + const take_cond = builder.add_if(make_space, add_new, { + cond: [f => f.add_getter('operator_gt', + { args: [ + [ + f => f.add_getter('data_lengthoflist', + {slots: { list: 'latest' }}), + 0 ], + 3] + }), + 0, + ] + }); + trigger.then_id(take_cond) + + const graph = builder.build(); + return graph; +} + +describe('Flow-92: [Reactive] Take N last values.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (if (> (list-length latest) 3) + ((delete-list-index latest 1))) + (add-to-list latest (flow-last-value "source" 0)) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/93_reactive_debounce.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/93_reactive_debounce.spec.ts new file mode 100644 index 00000000..f5a02b62 --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/93_reactive_debounce.spec.ts @@ -0,0 +1,66 @@ +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { compile } from '../../../flow-editor/graph_analysis'; +import { validate } from '../../../flow-editor/graph_validation'; +import { TIME_MONITOR_ID } from '../../../flow-editor/platform_facilities'; +import { gen_compiled } from '../scaffolding/graph-analysis-tools'; +import { dsl_to_ast } from '../scaffolding/graph-analysis-tools-ast-dsl'; +import { GraphBuilder } from '../scaffolding/graph-analysis-tools-graph-builder'; +import { are_equivalent_ast } from './utils.spec'; + +export function gen_flow(): FlowGraph { + const builder = new GraphBuilder(); + + // Stream section + const source = builder.add_stream('flow_utc_time', {id: 'source', message: 'UTC time'}); + + // Stepped section + const op = builder.add_op('logging_add_log', { args: [ [source, 0] ] + }); + const cond = builder.add_if(op, null, { + cond: [f => f.add_getter('operator_equals', { args: [ { from_variable: 'latest' }, + [ f => f.add_getter('flow_get_thread_id'), + 0 ] + ] }), 0] + }); + + builder + .add_trigger('trigger_on_signal', {args: [[source, 0]]}) + .then(f => f.add_op('data_setvariableto', { + args: [ + [f => f.add_getter('flow_get_thread_id'), 0], + ], + slots: { 'variable': 'latest' } + })) + .then(f => f.add_op('control_wait', {args: [ 1 ]})) + .then_id(cond) + ; + + const graph = builder.build(); + return graph; +} + +describe('Flow-93: [Reactive] Debounce operation.', () => { + it('Validation should pass', async () => { + expect(validate(gen_flow())) + .toBeTruthy() + }); + + + it('Should be able to compile', async () => { + const compiled_flow = compile(gen_flow()); + + const dsl_ast = dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (wait-for-monitor key: utc_time from_service: "${TIME_MONITOR_ID}") + (set-var latest (flow_get_thread_id)) + (wait-seconds 1) + (if (= (get-var latest) (flow_get_thread_id)) + ((log (flow-last-value "source" 0)))) + ` + ); + + const from_ast = [gen_compiled(dsl_ast)]; + + are_equivalent_ast(compiled_flow, from_ast); + }); +}); diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/_gen_graphs.utils.ts b/frontend/src/app/tests/logic/flow-graph-analysis/_gen_graphs.utils.ts new file mode 100644 index 00000000..f8226c5c --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/_gen_graphs.utils.ts @@ -0,0 +1,43 @@ +import { spawn } from 'child_process'; +import * as fs from 'fs'; +import { convert_to_graphviz } from '../scaffolding/utils'; +import * as util from 'util'; + +export function run(): Promise { + const files = fs.readdirSync(__dirname); + const candidates = files.filter((file: string) => file.match(/\d+.*.spec.ts$/)); + + let done_count = 0; + const promises = candidates.map((file: string) => { + return new Promise(async (resolve, reject) => { + try { + const mod_name = file.substr(0, file.length - 3); + const mod = require('./' + mod_name); // Remove '.ts' + if (mod.gen_flow) { + await util.promisify(fs.writeFile)(`${__dirname}/${file}.dot`, convert_to_graphviz(mod.gen_flow())); + spawn("dot", ["-Tpng", `${__dirname}/${file}.dot`, '-o', `${__dirname}/${file}.png`]); + + if (mod.process_flow) { + await util.promisify(fs.writeFile)(`${__dirname}/${file}.processed.dot`, + convert_to_graphviz(mod.process_flow(mod.gen_flow()))); + spawn("dot", ["-Tpng", `${__dirname}/${file}.processed.dot`, '-o', `${__dirname}/${file}.processed.png`]); + } + + done_count++; + process.stdout.write(`[${done_count}/${candidates.length}] ${file}\n`) + resolve(file); + } + else { + process.stdout.write('No flow found\n'); + resolve(null); + } + } + catch (err) { + process.stdout.write(file + ': ' + err.toString() + '\n'); + reject(err); + } + }); + }); + + return Promise.all(promises) as Promise; +} diff --git a/frontend/src/app/tests/logic/flow-graph-analysis/utils.spec.ts b/frontend/src/app/tests/logic/flow-graph-analysis/utils.spec.ts new file mode 100644 index 00000000..bdce2a0d --- /dev/null +++ b/frontend/src/app/tests/logic/flow-graph-analysis/utils.spec.ts @@ -0,0 +1,23 @@ +import { CompiledFlowGraph } from '../../../flow-editor/flow_graph'; +import { canonicalize_ast_list } from '../scaffolding/graph-analysis-tools'; +import { decompile_to_dsl } from '../scaffolding/graph-analysis-tools-dsl-decompiler'; + +export function are_equivalent_ast(actual: CompiledFlowGraph[], expected: CompiledFlowGraph[]) { + const expectationMsg = ` +╭──────────╮ +│ RESULT │ +╰──────────╯ + +${decompile_to_dsl(actual).join('\n;; Alternative\n')} + +╭──────────╮ +│ EXPECTED │ +╰──────────╯ + +${decompile_to_dsl(expected).join('\n;; Alternative\n')} +`; + + expect(canonicalize_ast_list(actual)) + .withContext(expectationMsg) + .toEqual(canonicalize_ast_list(expected), expectationMsg); +} diff --git a/frontend/src/app/tests/logic/samples/01_simple_flow.js b/frontend/src/app/tests/logic/samples/01_simple_flow.js new file mode 100644 index 00000000..846d1cf0 --- /dev/null +++ b/frontend/src/app/tests/logic/samples/01_simple_flow.js @@ -0,0 +1,397 @@ +module.exports = { + "nodes": { + "fc4bd63f-d4ff-4e15-8b09-a224e6e1c635": { + "data": { + "type": "enum_value_block", + "value": { + "options": { + "enum_namespace": "536bf266-fabf-44a6-ba89-a0c71b8db608", + "enum_name": "get_locations" + }, + "value_id": "12/36/057/7", + "value_text": "Vigo" + } + }, + "position": { + "x": 1046, + "y": 470 + } + }, + "f1b8670c-0001-4417-8a39-2c52f5140383": { + "data": { + "type": "enum_value_block", + "value": { + "options": { + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + "value_id": "-137414823", + "value_text": "Bot testing" + } + }, + "position": { + "x": 1069, + "y": 624 + } + }, + "918294c3-1e7d-4b2f-ab73-77188a4b89b0": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "getter", + "outputs": [ + { + "type": "string" + } + ], + "message": "Get today's max temperature for %i1", + "inputs": [ + { + "type": "enum", + "enum_namespace": "536bf266-fabf-44a6-ba89-a0c71b8db608", + "enum_name": "get_locations" + } + ], + "block_function": "services.536bf266-fabf-44a6-ba89-a0c71b8db608.get_today_max_in_place" + }, + "synthetic_input_count": 0, + "synthetic_output_count": 0 + } + }, + "position": { + "x": 1008.5, + "y": 535 + } + }, + "2b3b54d4-bcfa-4137-825f-ca52e2be4e96": { + "data": { + "type": "direct_value_block", + "value": { + "value": 0, + "type": "integer" + } + }, + "position": { + "x": 1277, + "y": 200 + } + }, + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "operation", + "outputs": [ + { + "type": "pulse" + } + ], + "message": "On channel %i1 say %i2", + "inputs": [ + { + "type": "pulse" + }, + { + "type": "enum", + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + { + "type": "string" + } + ], + "icon": "http://192.168.1.35:8888/api/v0/assets/icons/de5baefb-13da-457e-90a5-57a753da8891", + "block_function": "services.de5baefb-13da-457e-90a5-57a753da8891.send_message" + }, + "synthetic_input_count": 1, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 866.5, + "y": 679 + } + }, + "0b2f1836-aaf3-4e13-84b9-8041c3b5b4b8": { + "data": { + "type": "direct_value_block", + "value": { + "value": 11, + "type": "integer" + } + }, + "position": { + "x": 883, + "y": 237 + } + }, + "032d2a4e-bfe2-4635-a1cf-dc62692eead7": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "trigger", + "outputs": [ + { + "type": "pulse" + } + ], + "message": "When all true", + "inputs": [ + { + "type": "boolean" + }, + { + "type": "boolean" + }, + { + "type": "boolean" + } + ], + "icon": "/assets/logo-dark.png", + "extra_inputs": { + "type": "boolean", + "quantity": "any" + }, + "block_function": "trigger_when_all_true" + }, + "synthetic_input_count": 0, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 867.5, + "y": 391.5 + } + }, + "0f82640f-b40b-4053-981d-1fe0b2c17de0": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "icon": "/assets/logo-dark.png", + "message": "Are all equals?", + "block_function": "operator_equals", + "type": "getter", + "inputs": [ + { + "type": "any" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "extra_inputs": { + "type": "any", + "quantity": "any" + }, + "outputs": [ + { + "type": "boolean" + } + ] + }, + "synthetic_input_count": 0, + "synthetic_output_count": 0 + } + }, + "position": { + "x": 722.5, + "y": 293.5 + } + }, + "0d4937a0-cdd3-4bf6-b8e8-8da2974b1330": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "icon": "/assets/logo-dark.png", + "message": "Are all equals?", + "block_function": "operator_equals", + "type": "getter", + "inputs": [ + { + "type": "any" + }, + { + "type": "any" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "extra_inputs": { + "type": "any", + "quantity": "any" + }, + "outputs": [ + { + "type": "boolean" + } + ] + }, + "synthetic_input_count": 0, + "synthetic_output_count": 0 + } + }, + "position": { + "x": 1099.5, + "y": 283.5 + } + }, + "ad97e5d1-c725-4cc6-826f-30057f239635": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "icon": "/assets/logo-dark.png", + "message": "UTC time", + "block_function": "flow_utc_time", + "type": "getter", + "outputs": [ + { + "name": "hour", + "type": "integer" + }, + { + "name": "minute", + "type": "integer" + }, + { + "name": "second", + "type": "integer" + } + ], + "inputs": [] + }, + "synthetic_input_count": 0, + "synthetic_output_count": 0 + } + }, + "position": { + "x": 881.5, + "y": 117.5 + } + } + }, + "edges": [ + { + "from": { + "id": "f1b8670c-0001-4417-8a39-2c52f5140383", + "output_index": 0 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 1 + } + }, + { + "from": { + "id": "918294c3-1e7d-4b2f-ab73-77188a4b89b0", + "output_index": 0 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 2 + } + }, + { + "from": { + "id": "fc4bd63f-d4ff-4e15-8b09-a224e6e1c635", + "output_index": 0 + }, + "to": { + "id": "918294c3-1e7d-4b2f-ab73-77188a4b89b0", + "input_index": 0 + } + }, + { + "from": { + "id": "032d2a4e-bfe2-4635-a1cf-dc62692eead7", + "output_index": 0 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 0 + } + }, + { + "from": { + "id": "ad97e5d1-c725-4cc6-826f-30057f239635", + "output_index": 2 + }, + "to": { + "id": "0d4937a0-cdd3-4bf6-b8e8-8da2974b1330", + "input_index": 1 + } + }, + { + "from": { + "id": "ad97e5d1-c725-4cc6-826f-30057f239635", + "output_index": 1 + }, + "to": { + "id": "0d4937a0-cdd3-4bf6-b8e8-8da2974b1330", + "input_index": 0 + } + }, + { + "from": { + "id": "2b3b54d4-bcfa-4137-825f-ca52e2be4e96", + "output_index": 0 + }, + "to": { + "id": "0d4937a0-cdd3-4bf6-b8e8-8da2974b1330", + "input_index": 2 + } + }, + { + "from": { + "id": "ad97e5d1-c725-4cc6-826f-30057f239635", + "output_index": 0 + }, + "to": { + "id": "0f82640f-b40b-4053-981d-1fe0b2c17de0", + "input_index": 0 + } + }, + { + "from": { + "id": "0b2f1836-aaf3-4e13-84b9-8041c3b5b4b8", + "output_index": 0 + }, + "to": { + "id": "0f82640f-b40b-4053-981d-1fe0b2c17de0", + "input_index": 1 + } + }, + { + "from": { + "id": "0f82640f-b40b-4053-981d-1fe0b2c17de0", + "output_index": 0 + }, + "to": { + "id": "032d2a4e-bfe2-4635-a1cf-dc62692eead7", + "input_index": 0 + } + }, + { + "from": { + "id": "0d4937a0-cdd3-4bf6-b8e8-8da2974b1330", + "output_index": 0 + }, + "to": { + "id": "032d2a4e-bfe2-4635-a1cf-dc62692eead7", + "input_index": 1 + } + } + ] +} diff --git a/frontend/src/app/tests/logic/samples/02_lone_block.js b/frontend/src/app/tests/logic/samples/02_lone_block.js new file mode 100644 index 00000000..1964b10b --- /dev/null +++ b/frontend/src/app/tests/logic/samples/02_lone_block.js @@ -0,0 +1,79 @@ +module.exports = { + "nodes": { + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "operation", + "outputs": [ + { + "type": "pulse" + } + ], + "message": "On channel %i1 say %i2", + "inputs": [ + { + "type": "pulse" + }, + { + "type": "enum", + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + { + "type": "string" + } + ], + "icon": "http://192.168.1.35:8888/api/v0/assets/icons/de5baefb-13da-457e-90a5-57a753da8891", + "block_function": "services.de5baefb-13da-457e-90a5-57a753da8891.send_message" + }, + "synthetic_input_count": 1, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 866.5, + "y": 679 + } + }, + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "operation", + "outputs": [ + { + "type": "pulse" + } + ], + "message": "On channel %i1 say %i2", + "inputs": [ + { + "type": "pulse" + }, + { + "type": "enum", + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + { + "type": "string" + } + ], + "icon": "http://192.168.1.35:8888/api/v0/assets/icons/de5baefb-13da-457e-90a5-57a753da8891", + "block_function": "services.de5baefb-13da-457e-90a5-57a753da8891.send_message" + }, + "synthetic_input_count": 1, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 973.5, + "y": 484 + } + } + }, + "edges": [] +} diff --git a/frontend/src/app/tests/logic/samples/03_no_start_pulse.js b/frontend/src/app/tests/logic/samples/03_no_start_pulse.js new file mode 100644 index 00000000..6705b8b9 --- /dev/null +++ b/frontend/src/app/tests/logic/samples/03_no_start_pulse.js @@ -0,0 +1,153 @@ +module.exports = { + "nodes": { + "f1b8670c-0001-4417-8a39-2c52f5140383": { + "data": { + "type": "enum_value_block", + "value": { + "options": { + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + "value_id": "-137414823", + "value_text": "Bot testing" + } + }, + "position": { + "x": 1097, + "y": 622 + } + }, + "4652b79c-603b-4add-9164-92508be43fdf": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "operation", + "outputs": [ + { + "type": "pulse" + }, + { + "type": "string" + } + ], + "message": "Get today's max temperature for %i1", + "inputs": [ + { + "type": "pulse" + }, + { + "type": "enum", + "enum_namespace": "536bf266-fabf-44a6-ba89-a0c71b8db608", + "enum_name": "get_locations" + } + ], + "block_function": "services.536bf266-fabf-44a6-ba89-a0c71b8db608.get_today_max_in_place" + }, + "synthetic_input_count": 1, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 902.5, + "y": 531 + } + }, + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "operation", + "outputs": [ + { + "type": "pulse" + } + ], + "message": "On channel %i1 say %i2", + "inputs": [ + { + "type": "pulse" + }, + { + "type": "enum", + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + { + "type": "string" + } + ], + "icon": "http://192.168.1.35:8888/api/v0/assets/icons/de5baefb-13da-457e-90a5-57a753da8891", + "block_function": "services.de5baefb-13da-457e-90a5-57a753da8891.send_message" + }, + "synthetic_input_count": 1, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 866.5, + "y": 679 + } + }, + "099ee247-851d-41bb-819c-5347247cd06a": { + "data": { + "type": "enum_value_block", + "value": { + "options": { + "enum_namespace": "536bf266-fabf-44a6-ba89-a0c71b8db608", + "enum_name": "get_locations" + }, + "value_id": "12/36/057/7", + "value_text": "Vigo" + } + }, + "position": { + "x": 1094, + "y": 474 + } + } + }, + "edges": [ + { + "from": { + "id": "099ee247-851d-41bb-819c-5347247cd06a", + "output_index": 0 + }, + "to": { + "id": "4652b79c-603b-4add-9164-92508be43fdf", + "input_index": 1 + } + }, + { + "from": { + "id": "4652b79c-603b-4add-9164-92508be43fdf", + "output_index": 1 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 2 + } + }, + { + "from": { + "id": "f1b8670c-0001-4417-8a39-2c52f5140383", + "output_index": 0 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 1 + } + }, + { + "from": { + "id": "4652b79c-603b-4add-9164-92508be43fdf", + "output_index": 0 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 0 + } + } + ] +} diff --git a/frontend/src/app/tests/logic/samples/04_no_start_loop.js b/frontend/src/app/tests/logic/samples/04_no_start_loop.js new file mode 100644 index 00000000..de4f47b1 --- /dev/null +++ b/frontend/src/app/tests/logic/samples/04_no_start_loop.js @@ -0,0 +1,163 @@ +module.exports = { + "nodes": { + "f1b8670c-0001-4417-8a39-2c52f5140383": { + "data": { + "type": "enum_value_block", + "value": { + "options": { + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + "value_id": "-137414823", + "value_text": "Bot testing" + } + }, + "position": { + "x": 1069, + "y": 624 + } + }, + "4652b79c-603b-4add-9164-92508be43fdf": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "getter", + "outputs": [ + { + "type": "pulse" + }, + { + "type": "string" + } + ], + "message": "Get today's max temperature for %i1", + "inputs": [ + { + "type": "pulse" + }, + { + "type": "enum", + "enum_namespace": "536bf266-fabf-44a6-ba89-a0c71b8db608", + "enum_name": "get_locations" + } + ], + "block_function": "services.536bf266-fabf-44a6-ba89-a0c71b8db608.get_today_max_in_place" + }, + "synthetic_input_count": 1, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 903.5, + "y": 532 + } + }, + "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c": { + "data": { + "type": "simple_flow_block", + "value": { + "options": { + "type": "operation", + "outputs": [ + { + "type": "pulse" + } + ], + "message": "On channel %i1 say %i2", + "inputs": [ + { + "type": "pulse" + }, + { + "type": "enum", + "enum_namespace": "de5baefb-13da-457e-90a5-57a753da8891", + "enum_name": "get_known_channels" + }, + { + "type": "string" + } + ], + "icon": "http://192.168.1.35:8888/api/v0/assets/icons/de5baefb-13da-457e-90a5-57a753da8891", + "block_function": "services.de5baefb-13da-457e-90a5-57a753da8891.send_message" + }, + "synthetic_input_count": 1, + "synthetic_output_count": 1 + } + }, + "position": { + "x": 867.5, + "y": 687 + } + }, + "099ee247-851d-41bb-819c-5347247cd06a": { + "data": { + "type": "enum_value_block", + "value": { + "options": { + "enum_namespace": "536bf266-fabf-44a6-ba89-a0c71b8db608", + "enum_name": "get_locations" + }, + "value_id": "12/36/057/7", + "value_text": "Vigo" + } + }, + "position": { + "x": 1052, + "y": 425 + } + } + }, + "edges": [ + { + "from": { + "id": "099ee247-851d-41bb-819c-5347247cd06a", + "output_index": 0 + }, + "to": { + "id": "4652b79c-603b-4add-9164-92508be43fdf", + "input_index": 1 + } + }, + { + "from": { + "id": "4652b79c-603b-4add-9164-92508be43fdf", + "output_index": 1 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 2 + } + }, + { + "from": { + "id": "f1b8670c-0001-4417-8a39-2c52f5140383", + "output_index": 0 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 1 + } + }, + { + "from": { + "id": "4652b79c-603b-4add-9164-92508be43fdf", + "output_index": 0 + }, + "to": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "input_index": 0 + } + }, + { + "from": { + "id": "1545b4f2-8b4f-4c59-ae0b-a0a0e5d6746c", + "output_index": 0 + }, + "to": { + "id": "4652b79c-603b-4add-9164-92508be43fdf", + "input_index": 0 + } + } + ] +} diff --git a/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-ast-dsl.ts b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-ast-dsl.ts new file mode 100644 index 00000000..c81b60b5 --- /dev/null +++ b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-ast-dsl.ts @@ -0,0 +1,297 @@ +import { SimpleArrayAst, SimpleArrayAstArgument, SimpleArrayAstOperation } from './graph-analysis-tools'; + +const SYMBOL_CHARACTERS = ( + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + + '0123456789' + + '!$\'",_-./:;?+<=>#%&*@[\]{|}`^~' +); + +export const OP_TRANSLATIONS: {[key: string]: string} = { + // Flow control + 'if': 'control_if_else', + 'jump-to': 'jump_to_block', + 'jump-point': 'jump_point', + 'fork': 'op_fork_execution', + 'repeat': 'control_repeat', + + // Operations + 'log': 'logging_add_log', + 'wait-seconds': 'control_wait', + 'call-service': 'command_call_service', + 'preload': 'op_preload_getter', + 'on-block-run': 'op_on_block_run', + + // Variables + 'set-var': 'data_setvariableto', + 'on-var': 'on_data_variable_update', + 'get-var': 'data_variable', + 'flow-last-value': 'flow_last_value', + 'wait-for-monitor': 'wait_for_monitor', + + // Comparations + '=': 'operator_equals', + 'and': 'operator_and', + '<': 'operator_lt', + '>': 'operator_gt', + + // Operations + 'mod': 'operator_modulo', + '+': 'operator_add', + 'add-to-list': 'data_addtolist', + 'list-length': 'data_lengthoflist', + 'delete-list-index': 'data_deleteoflist', +}; + +const OPS_WITH_MAP_ARGUMENTS = [ + 'wait_for_monitor', 'command_call_service', +]; + +function is_digit(c: string) { + return '0123456789'.indexOf(c) >= 0; +} + +function array_to_map(args: any[]): {[key: string]: any} { + if ((args.length % 2) > 0) { + throw new Error("Odd number of arguments on function call, expected even."); + } + + const result: {[key: string]: any} = {}; + + for (let idx=0; idx < args.length; idx+=2) { + let key = args[idx]; + const value = args[idx+1]; + + if (key.indexOf(':') >= 0) { + if (key.indexOf(':') != (key.length - 1)) { + throw new Error("Character ':' allowed ONLY at the end of symbol, not before"); + } + + key = key.substring(0, key.length - 1); + } + + result[key] = value; + } + + return result; +} + +function transform_call(args: any[]): any[] { + if (args.length == 0) { + return args; + } + + let op = args[0] as string; + if (OP_TRANSLATIONS[op]) { + op = args[0] = OP_TRANSLATIONS[op]; + } + + if (OPS_WITH_MAP_ARGUMENTS.indexOf(op) >= 0) { + args = [op, array_to_map(args.slice(1))]; + } + + if (op === 'wait_for_monitor') { + if (args[1].from_service) { + args[1].monitor_id = { from_service: args[1].from_service }; + delete args[1].from_service; + } + if (!args[1].monitor_expected_value) { + args[1].monitor_expected_value = 'any_value'; + } + } + + if (op === 'command_call_service') { + if (args[1].id) { + args[1].service_id = args[1].id; + delete args[1].id; + } + if (args[1].action) { + args[1].service_action = args[1].action; + delete args[1].action; + } + if (args[1].values) { + args[1].service_call_values = args[1].values; + delete args[1].values; + } + } + + if (op === 'op_fork_execution') { + let kw_args_index: number; + for (kw_args_index = 1; kw_args_index < args.length; kw_args_index++) { + const arg = args[kw_args_index]; + + if (typeof arg !== 'string') { + break; + } + + const colon_pos = arg.indexOf(':'); + if (colon_pos >= 0) { + if (colon_pos === 0 && (arg.substring(1).indexOf(':') < 0)) { + args[kw_args_index] = arg.substring(1); + } + else if (colon_pos === (arg.length - 1)) { + args[kw_args_index] = arg.substring(0, arg.length - 1); + } + else { + throw new Error(`Character ':' allowed ONLY at beginning or end of symbol. On (op:${op}, arg: ${arg})`); + } + + } + } + + let contents = []; + let kw_args = []; + + if (args.length > kw_args_index) { + contents = args.splice(kw_args_index); + } + if (args.length > 1) { + kw_args = args.splice(1); + } + + // Translate direct operations (in contents) with an operation list + for (let idx = 0; idx < contents.length; idx++) { + if (typeof contents[idx][0] === 'string') { + contents[idx] = [contents[idx]]; + } + } + + args[1] = kw_args; + args[2] = contents; + } + + return args; +} + +function read(s: string, idx: number, linenum: number, colnum: number): [SimpleArrayAstArgument, number, number, number] { + let op: SimpleArrayAstArgument = null; + + if (s[idx] === ';') { + // Wait for the end of the line + do { + idx++; + } while (s[idx] !== '\n'); + idx++; // Jump over next \n + + colnum = 1; + linenum++; + } + else if (s[idx] === '(') { + const start = [idx, linenum, colnum]; + + idx++; + colnum++; + + op = [] as any; + while ((idx < s.length) && (s[idx] !== ')')) { + + let token: SimpleArrayAstArgument; + [token, idx, linenum, colnum] = read(s, idx, linenum, colnum); + if (token !== null) { + (op as any).push(token); + } + } + if (idx >= s.length) { + throw new Error(`${start[1]}:${start[2]} Unclosed list`); + } + if (s[idx] !== ')') { + throw new Error(`${start[1]}:${start[2]} Unexpected state on list close (this should not happen)`); + } + + try { + op = transform_call(op as any) as any; + } + catch (err) { + err.message = `On call starting at ${start[1]}:${start[2]}: ${err.message}`; + throw err; + } + + idx++; + colnum++; + } + else if (s[idx] === ')') { + throw new Error(`${linenum}:${colnum} Unmatched list close ')'`) + } + else if (s[idx] === '"') { + const token = []; + const start = [idx, linenum, colnum]; + let just_escaped = false; + + idx++; + colnum++; + while ((s[idx] !== '"' ) || just_escaped) { + if (just_escaped) { + // TODO: Convert current character + just_escaped = false; + throw new Error('NOT IMPLEMENTED'); + } + else { + if (s[idx] === '\\') { + just_escaped = true; + } + else if (s[idx] === '\n') { + throw new Error(`${linenum}:${colnum} Unended string starting at ${start[1]}:${start[2]}`) + } + else { + token.push(s[idx]); + } + } + idx++; + colnum++; + + } + // Jump over end " + idx++; + colnum++; + + op = token.join(''); + } + else if (' \t'.indexOf(s[idx]) >= 0) { + idx++; + colnum++; // Whitespace + } + else if (s[idx] === '\n') { + idx++; + colnum = 1; + linenum++; + } + else if (SYMBOL_CHARACTERS.indexOf(s[idx]) >= 0) { + const token = []; + + while (SYMBOL_CHARACTERS.indexOf(s[idx]) >= 0) { + token.push(s[idx]); + + idx++; + colnum++; + } + + op = token.join(''); + if (token.filter(c => is_digit(c)).length === token.length) { // All are digits + op = parseInt(op); + } + else if (op.match(/(^\d*\.\d+$)|(^\d+\.\d$)/)) { + op = parseFloat(op); + } + } + else { + throw new Error(`${linenum}:${colnum} Unexpected character ${s[idx]}`) + } + + return [op, idx, linenum, colnum]; +} + +export function dsl_to_ast(s: string): SimpleArrayAst { + const ast: SimpleArrayAstOperation[] = []; + + let linenum = 1; + let colnum = 1; + + for (let idx = 0; idx < s.length;) { + let token: SimpleArrayAstArgument; + [token, idx, linenum, colnum] = read(s, idx, linenum, colnum); + if (token !== null) { + ast.push(token as SimpleArrayAstOperation); + } + } + + return ast; +} diff --git a/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-dsl-decompiler.ts b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-dsl-decompiler.ts new file mode 100644 index 00000000..6eba2472 --- /dev/null +++ b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-dsl-decompiler.ts @@ -0,0 +1,106 @@ +import { CompiledBlock, CompiledBlockArg, CompiledBlockArgCallServiceDict, CompiledBlockArgMonitorDict, CompiledBlockArgs, CompiledFlowGraph, ContentBlock, CompiledBlockType } from '../../../flow-editor/flow_graph'; + +import { OP_TRANSLATIONS } from './graph-analysis-tools-ast-dsl'; + +const OP_REVERSE_TRANSLATIONS: {[key: string]: string} = {}; +for (const op of Object.keys(OP_TRANSLATIONS)) { + OP_REVERSE_TRANSLATIONS[OP_TRANSLATIONS[op]] = op; +} + +function repr_single_arg(arg: CompiledBlockArg, depth: number): string { + if (arg.type === 'constant') { + return JSON.stringify(arg.value); + } + else if (arg.type === 'variable' || arg.type === 'list') { + return arg.value; + } + else if (arg.type === 'block') { + return repr_contents(arg.value, depth, { skip_first_indent: true }); + } +} + +function repr_args(args: CompiledBlockArgs, depth: number): string { + if (args instanceof Array) { + return args.map(arg => repr_single_arg(arg, depth)).join(' '); + } + else if ((args as CompiledBlockArgMonitorDict).monitor_id) { + const mon_args = (args as CompiledBlockArgMonitorDict); + const value = JSON.stringify(mon_args.monitor_expected_value); + return `key: ${mon_args.key} from_service: "${mon_args.monitor_id.from_service}" monitor_expected_value: ${value}` + } + else if ((args as CompiledBlockArgCallServiceDict).service_id) { + const call_args = (args as CompiledBlockArgCallServiceDict); + const values = repr_args(call_args.service_call_values, depth); + return `id: ${call_args.service_id} action: ${call_args.service_action} values: (${values})`; + } + else if ((args as any).key) { + const selector = args as any; + if (selector.subkey) { + return `key: ${selector.key} subkey: ${selector.subkey}`; + } + return `key: ${selector.key}`; + } + else { + throw new Error(`Unknown args type: ${JSON.stringify(args)}`) + } +} + +function repr_contents(contents: (CompiledBlock|ContentBlock)[], depth: number, options?: {skip_first_indent?: boolean}): string { + const results = []; + for (const content of contents) { + if ((content as CompiledBlock).type) { + results.push(gen_tree(content as CompiledBlock, depth)); + } + else { + results.push(`(${repr_contents(content.contents, depth + 1, { skip_first_indent: true })})`); + } + } + + if (results.length === 0) { + return ''; + } + + let prefix = ''; + if (!options || !options.skip_first_indent) { + prefix = indentation_for_depth(depth); + } + return prefix + results.join('\n' + indentation_for_depth(depth)); +} + +function indentation_for_depth(depth: number) { + const character = ' '; + + return Array(depth).fill(character).join(''); +} + +function gen_tree(block: CompiledBlock, depth: number): string { + const args = repr_args(block.args, depth); + let type = block.type; + if (OP_REVERSE_TRANSLATIONS[type]) { + type = OP_REVERSE_TRANSLATIONS[type] as CompiledBlockType; + } + + const depth_increase = 1 + type.length + 2; // '(' operation ' ' * 2 + + if (block.contents && block.contents.length > 0) { + const contents = repr_contents(block.contents, depth + depth_increase); + return `(${type} ${args}\n${contents})`; + } + else { + return `(${type} ${args})`; + } +} + +function decompile_flow(flow: CompiledFlowGraph): string { + const tokens = []; + + for (const op of flow) { + tokens.push(gen_tree(op, 0)); + } + + return tokens.join('\n'); +} + +export function decompile_to_dsl(flows: CompiledFlowGraph[]): string[] { + return flows.map(flow => decompile_flow(flow)); +} diff --git a/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-graph-builder.ts b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-graph-builder.ts new file mode 100644 index 00000000..b2416133 --- /dev/null +++ b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools-graph-builder.ts @@ -0,0 +1,437 @@ +import { AtomicFlowBlockData, AtomicFlowBlockOptions, BLOCK_TYPE as ATOMIC_BLOCK_TYPE, AtomicFlowBlock, AtomicFlowBlockOperationType, isAtomicFlowBlockOptions } from '../../../flow-editor/atomic_flow_block'; +import { BaseToolboxDescription, ToolboxDescription } from '../../../flow-editor/base_toolbox_description'; +import { BLOCK_TYPE as VALUE_BLOCK_TYPE, DirectValueFlowBlockData } from '../../../flow-editor/direct_value'; +import { BLOCK_TYPE as ENUM_BLOCK_TYPE, EnumDirectValueFlowBlockData, EnumDirectValueOptions } from '../../../flow-editor/enum_direct_value'; +import { MessageType, InputPortDefinition } from '../../../flow-editor/flow_block'; +import { FlowGraph, FlowGraphEdge, FlowGraphNode } from '../../../flow-editor/flow_graph'; +import { uuidv4 } from '../../../flow-editor/utils'; +import { UiToolboxDescription } from '../../../flow-editor/ui-blocks/ui_toolbox_description'; +import { UiFlowBlockOptions, isUiFlowBlockOptions, BLOCK_TYPE as FLOW_BLOCK_TYPE, UiFlowBlockData } from '../../../flow-editor/ui-blocks/ui_flow_block'; +import { is_pulse } from '../../../flow-editor/graph_transformations'; + + + +type NodeDescription = AtomicFlowBlockData | EnumDirectValueFlowBlockData | DirectValueFlowBlockData | UiFlowBlockData; + +type ValueNodeRef = [string, number]; +type StreamGenerator = (builder: GraphBuilder) => StreamNodeBuilderRef; +type NodeGenerator = (builder: GraphBuilder) => OpNodeBuilderRef; + +type NodeRef = ValueNodeRef | StreamNodeBuilderRef | OpNodeBuilderRef; +type VariableRef = {from_variable: string}; + +type BlockArgument = [OpNodeBuilderRef, 'pulse'] + | [NodeRef, number] + | [StreamGenerator, number] + | ValueNodeRef + | VariableRef + | string + | number +; + + +function index_toolbox_description(desc: ToolboxDescription): {[key: string]: AtomicFlowBlockOptions | UiFlowBlockOptions} { + const result: {[key: string]: AtomicFlowBlockOptions | UiFlowBlockOptions} = {}; + + 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; + } + else if (isUiFlowBlockOptions(block)) { + result[block.id] = block; + } + } + } + + return result; +} + +function infer_block_options(block_type: string, + options: BlockOptions, + params: { type: AtomicFlowBlockOperationType }): AtomicFlowBlockOptions { + + const inputs = []; + for (const _arg of options.args || []) { + inputs.push({ type: 'any' }); + } + + return { + message: block_type, + block_function: `services.${options.namespace}.${block_type}`, + type: params.type, + inputs: inputs as InputPortDefinition[], + outputs: [ + { + type: 'any', + } + ] + }; +} + + +const BaseBlocks = index_toolbox_description([...BaseToolboxDescription, ...UiToolboxDescription]); + +interface BlockOptions { + namespace?: string, + message?: string, + args?: BlockArgument[], + id?: string, + slots?: {[key: string]: string}; +} + +class StreamNodeBuilderRef { + builder: GraphBuilder; + id: string; + + constructor(builder: GraphBuilder, id: string) { + this.builder = builder; + this.id = id; + } +} + +class OpNodeBuilderRef { + builder: GraphBuilder; + id: string; + + constructor(builder: GraphBuilder, id: string) { + this.builder = builder; + this.id = id; + } + + then_id(next: string) { + // Separated from `then` as this cannot guarantee that a + // OpNodeBuilderRef that also can perform `.then` can be returned + this.builder.establish_connection([this.id, 0], [next, 0]); + } + + then(next: NodeGenerator | OpNodeBuilderRef, originSignalPosition=0, targetSignalPosition=0): OpNodeBuilderRef { + if ((next as OpNodeBuilderRef).id) { + const nextOp = (next as OpNodeBuilderRef); + this.builder.establish_connection([this.id, originSignalPosition], [nextOp.id, targetSignalPosition]); + + return nextOp; + } + else { + const nextGen = next as NodeGenerator; + const nextOp = nextGen(this.builder); + this.builder.establish_connection([this.id, originSignalPosition], [nextOp.id, targetSignalPosition]); + + return nextOp; + } + } +} + +export class GraphBuilder { + nodes: {[key: string]: NodeDescription} = {}; + edges: FlowGraphEdge[] = []; + blocks: {[key: string]: AtomicFlowBlockOptions | UiFlowBlockOptions} = {}; + + constructor() { + this.blocks = BaseBlocks; + } + + add_service(service_id?: string): string { + if (!service_id) { + service_id = uuidv4(); + } + + return service_id; + } + + + add_enum_node(namespace: string, name: string, value: string, id: string, options?: { id: string }): ValueNodeRef { + const ref = options && options.id ? options.id : 'enum_' + name + '_' + uuidv4(); + + this.nodes[ref] = { + type: ENUM_BLOCK_TYPE, + value: { + value_id: id, + value_text: value, + options: { + definition: { + type: 'enum', + enum_name: name, + enum_namespace: namespace, + }, + } as EnumDirectValueOptions + } + } + + return [ref, 0]; + } + + private add_direct_node(value: any): ValueNodeRef { + const ref = 'dir_' + value.toString().toLowerCase() + '_' + uuidv4(); + + let type: MessageType = 'any'; + if (typeof value === 'string') { + type = 'string'; + } + else if (typeof value === 'number') { + if (value % 1 === 0) { + type = 'integer'; + } + else { + type = 'float'; + } + } + + this.nodes[ref] = { + type: VALUE_BLOCK_TYPE, + value: { + value: type === 'string' ? value + '' : value, + type: type, + } + }; + + return [ref, 0]; + + } + + public add_variable_getter_node(var_name: any, options?: { id: string }): StreamNodeBuilderRef { + const id = options && options.id ? options.id : 'get_var_' + var_name + '_' + uuidv4(); + + return this.add_getter('data_variable', { id: id, slots: { variable: var_name } }); + } + + private add_node(block_options: BlockOptions, ref: string): number { + let synth_in = 0, synth_out = 0; + if (isAtomicFlowBlockOptions(block_options)) { + [block_options, synth_in, synth_out] = AtomicFlowBlock.add_synth_io(block_options); + this.nodes[ref] = { + type: ATOMIC_BLOCK_TYPE, + value: { + options: block_options as AtomicFlowBlockOptions, + slots: block_options.slots || {}, + synthetic_input_count: synth_in, + synthetic_output_count: synth_out, + } + } + } + else if (isUiFlowBlockOptions(block_options)) { + this.nodes[ref] = { + type: FLOW_BLOCK_TYPE, + value: { + options: block_options, + extra: {}, + } + } + } + else { + throw new Error(`Unexpected flow block options: ${JSON.stringify(block_options)}`); + } + + return synth_in; + } + + private resolve_args(node: string, options?: BlockOptions, offset?: number) { + if (!options) { + return; + } + + let args = options.args || []; + + if (args) { + let idx = -1; + + if (offset) { + idx += offset; + } + + for (const arg of args) { + idx++; + + if (arg === null || arg === undefined) { continue; } + + // Direct value + if (typeof arg === 'number' || typeof arg === 'string') { + const ref = this.add_direct_node(arg); + this.establish_connection(ref, [node, idx]); + } + // Variable reference + else if ((arg as VariableRef).from_variable) { + const vblock = this.add_variable_getter_node((arg as VariableRef).from_variable); + + this.establish_connection([ vblock.id, 0 ], [node, idx]); + } + // Direct node reference + else if (typeof ((arg as any)[0]) === 'string') { + this.establish_connection(arg as [string, number], [node, idx]); + } + // Node reference + else if (((arg as any)[0] as StreamNodeBuilderRef).id) { + let out_index = (arg as any)[1] as number; + if ((arg as any)[1] === 'pulse'){ + out_index = 0; + } + this.establish_connection([((arg as any)[0] as StreamNodeBuilderRef).id, out_index], [node, idx]); + } + // Generator + else { + let out_index = (arg as any)[1] as number; + if ((arg as any)[1] === 'pulse'){ + out_index = 0; + } + + const gen = (arg as any)[0] as StreamGenerator; + + this.establish_connection([gen(this).id, out_index], [node, idx]); + } + } + } + } + + add_stream(block_type: string, options?: BlockOptions): StreamNodeBuilderRef { + const ref = options.id ? options.id : (block_type + '_' + uuidv4()); + + let block_options = this.blocks[block_type]; + if (!block_options) { + if (options && options.namespace) { + block_options = infer_block_options(block_type, options, { type: 'getter' }); + } + else { + throw new Error(`Unknown block type: ${block_type}`); + } + } + + const synth_in = this.add_node(block_options, ref); + this.resolve_args(ref, options, synth_in); + + return new StreamNodeBuilderRef(this, ref); + } + + add_trigger(block_type: string, options?: BlockOptions): OpNodeBuilderRef { + const ref = options.id ? options.id : (block_type + '_' + uuidv4()); + + let block_options = this.blocks[block_type]; + if (!block_options) { + if (options && options.namespace) { + block_options = infer_block_options(block_type, options, { type: 'trigger' }); + } + else { + throw new Error(`Unknown block type: ${block_type}`); + } + } + + if (options && options.slots) { + block_options.slots = Object.assign(block_options.slots || {}, options.slots) + } + + const synth_in = this.add_node(block_options, ref); + this.resolve_args(ref, options, synth_in); + + return new OpNodeBuilderRef(this, ref); + } + + add_getter(block_type: string, options?: BlockOptions): StreamNodeBuilderRef { + const ref = options && options.id ? options.id : (block_type + '_' + uuidv4()); + + let block_options = this.blocks[block_type]; + + if (!block_options) { + block_options = infer_block_options(block_type, options, { type: 'getter' }); + } + + if (options && options.slots) { + block_options.slots = Object.assign(block_options.slots || {}, options.slots) + } + + const synth_in = this.add_node(block_options, ref); + this.resolve_args(ref, options, synth_in); + + return new StreamNodeBuilderRef(this, ref); + } + + add_op(block_type: string, options?: BlockOptions): OpNodeBuilderRef { + const ref = options.id ? options.id : (block_type + '_' + uuidv4()); + + let block_options = this.blocks[block_type]; + + if (!block_options) { + block_options = infer_block_options(block_type, options, { type: 'operation' }); + } + + if (options && options.slots) { + block_options.slots = Object.assign(block_options.slots || {}, options.slots) + } + + const pulse_inputs = block_options.inputs?.filter(p => is_pulse(p)).length || 0; + + const synth_in = this.add_node(block_options, ref); + this.resolve_args(ref, options, synth_in + pulse_inputs); + + return new OpNodeBuilderRef(this, ref); + } + + add_fork(source: OpNodeBuilderRef, branches: OpNodeBuilderRef[], options?: { id?: string }): string { + if (!options) { options = {} }; + + const block_type = 'op_fork_execution'; + + const ref = options.id ? options.id : (block_type + '_' + uuidv4()); + + let block_options = this.blocks[block_type]; + + this.add_node(block_options, ref); + this.establish_connection([source.id, 0], [ref, 0]); + let out_index = -1; + for (const branch of branches) { + out_index++; + + if (branch) { // Represent null as unused output port + this.establish_connection([ref, out_index], [branch.id, 0]); + } + } + + return ref; + } + + add_if(if_true: OpNodeBuilderRef, if_false: OpNodeBuilderRef, options: { id?: string, cond: BlockArgument | StreamGenerator }): string { + const block_type = 'control_if_else'; + + const ref = options.id ? options.id : (block_type + '_' + uuidv4()); + + let block_options = this.blocks[block_type]; + + const synth_in = this.add_node(block_options, ref); + + let cond = options.cond; + if (typeof cond === 'function') { + cond = [(cond as StreamGenerator)(this), 0]; + } + + this.resolve_args(ref, { args: [ cond ] }, synth_in); + if (if_true) { + this.establish_connection([ref, 0], [if_true.id, 0]); + } + if (if_false) { + this.establish_connection([ref, 1], [if_false.id, 0]); + } + + return ref; + } + + establish_connection(source: [string, number], sink: [string, number]) { + this.edges.push({ + from: { id: source[0], output_index: source[1] }, + to: { id: sink[0], input_index: sink[1] }, + }); + } + + build(): FlowGraph { + const nodes: {[key: string]: FlowGraphNode} = {}; + + for (const node_id of Object.keys(this.nodes)){ + const node = this.nodes[node_id]; + + nodes[node_id] = {data: node, position: null}; + } + + return { nodes: nodes, + edges: this.edges, + }; + + } +} diff --git a/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools.ts b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools.ts new file mode 100644 index 00000000..f9939827 --- /dev/null +++ b/frontend/src/app/tests/logic/scaffolding/graph-analysis-tools.ts @@ -0,0 +1,290 @@ +import { CompiledBlock, CompiledBlockArg, CompiledBlockArgCallServiceDict, CompiledBlockArgList, CompiledFlowGraph, ContentBlock } from '../../../flow-editor/flow_graph'; +import { _link_graph } from '../../../flow-editor/graph_analysis'; +const stable_stringify = require('fast-json-stable-stringify'); + +type _AndOp = ['operator_and', SimpleArrayAstArgument, SimpleArrayAstArgument]; +type _EqualsOp = ['operator_equals', SimpleArrayAstArgument, SimpleArrayAstArgument] + | ['operator_equals', SimpleArrayAstArgument, SimpleArrayAstArgument, SimpleArrayAstArgument]; +type _CallServiceOp = ['command_call_service', + { service_id: string, service_action: string, service_call_values: SimpleArrayAstArgument[] }]; +type _WaitForMonitorOp = ['wait_for_monitor', { monitor_id: { from_service: string }, key: "utc_time" | "utc_date", monitor_expected_value: any }]; +type _LastValueOp = ['flow_last_value', string, number | string]; +type _IfElseOp = ['control_if_else', SimpleArrayAstArgument, SimpleArrayAstOperation[]]; +type _RepeatOp = ['control_repeat', SimpleArrayAstArgument, SimpleArrayAstOperation[]]; +type _ForkExecOp = ['op_fork_execution', SimpleArrayAstArgument[], SimpleArrayAstOperation[]] + +export type SimpleArrayAstArgument = SimpleArrayAstOperation | string | number +export type SimpleArrayAstArgs = _AndOp | _EqualsOp + | _CallServiceOp + | _WaitForMonitorOp | _LastValueOp + | _IfElseOp | _RepeatOp + | _ForkExecOp +; + +export type SimpleArrayAstOperation = SimpleArrayAstArgs; + +export type SimpleArrayAst = SimpleArrayAstOperation[]; + +const SLOT_OPS = { + data_variable: 'variable', + on_data_variable_update: 'variable', + data_setvariableto: 'variable', + + data_lengthoflist: 'list', + data_deleteoflist: 'list', + data_addtolist: 'list', +}; + +function convert_argument(arg: SimpleArrayAstArgument): CompiledBlockArg { + if ((typeof arg === 'string') || (typeof arg === 'number')) { + return { + type: 'constant', + value: arg, + }; + } + + return { + type: 'block', + value: [convert_operation(arg)], + }; +} + +function convert_ast(ast: SimpleArrayAstOperation[]): CompiledBlock[] { + const result = []; + for (let idx = 0; idx < ast.length; idx++) { + const op = convert_operation(ast[idx]); + result.push(op); + } + + return result; +} + +function convert_contents(contents: SimpleArrayAstOperation[]): ContentBlock { + return { + contents: convert_ast(contents) + } +} + +function convert_operation(op: SimpleArrayAstOperation): CompiledBlock { + if (op[0] === 'wait_for_monitor') { + return { + type: op[0], + args: op[1], + contents: [], + } + } + + if (op[0] === 'command_call_service') { + return { + type: op[0], + args: { + service_action: op[1].service_action, + service_id: op[1].service_id, + service_call_values: op[1].service_call_values.map(v => convert_argument(v)) + }, + contents: [], + } + } + + if (op[0] === 'control_if_else') { + const contents = (op.slice(2) as SimpleArrayAstOperation[][]).map(v => convert_contents(v)); + + if (contents.length < 2) { + contents.push({ contents: [] }); + } + + return { + type: op[0], + args: [ convert_argument(op[1]) ], + contents: contents, + } + } + + if (op[0] === 'control_repeat') { + const contents = op.slice(2) as SimpleArrayAstOperation[]; + + return { + type: op[0], + args: [ convert_argument(op[1]) ], + contents: convert_ast(contents), + } + } + + if (op[0] === 'op_fork_execution') { + const contents = (op[2] as SimpleArrayAstOperation[][]).map(v => convert_contents(v)); + + if (contents.length < 2) { + console.warn('Fork (op_fork_execution) with less than two outward paths'); + } + + return { + type: op[0], + args: op[1].map(arg => convert_argument(arg)), + contents: contents, + } + } + + if (op[0].startsWith('services.') && (!op[0].startsWith('services.ui.'))) { + return { + type: op[0], + args: { + key: op[0].split('.').reverse()[0] + }, + }; + } + + if (typeof op !== 'object') { + throw new Error(`ASTCompilationError: Expected argument array, found: ${JSON.stringify(op)}. Check for errors on argument nesting`); + } + + const compiled_args = (op.slice(1) as SimpleArrayAstArgument[]).map(v => convert_argument(v)); + + if (op[0] in SLOT_OPS) { + // This IF is actually covered in the previous one, it's just here to make TypeScript happy. + if (op[0] !== 'operator_and' && op[0] !== 'operator_equals' && op[0] !== 'flow_last_value') { + compiled_args[0].type = SLOT_OPS[op[0]]; + } + } + + return { + type: op[0], + args: compiled_args, + contents: [] + } +} + +export function gen_compiled(ast: SimpleArrayAst): CompiledFlowGraph { + const result = convert_ast(ast); + return _link_graph(result); +} + +function canonicalize_arg(arg: CompiledBlockArg): CompiledBlockArg { + if (arg.type === 'constant') { + return arg; + } + else if (arg.type === 'variable' || arg.type === 'list') { + return arg; + } + else { + return { + type: arg.type, + value: arg.value.map((b: CompiledBlock) => canonicalize_op(b)), + } + } +} + +function canonicalize_content(content: (CompiledBlock | ContentBlock)): (CompiledBlock | ContentBlock) { + if ((content as CompiledBlock).type) { + return canonicalize_op((content as CompiledBlock)); + } + else { + return {contents: content.contents.map(c => canonicalize_content(c))}; + } +} + +function canonicalize_op(op: CompiledBlock): CompiledBlock { + delete op.id; + + switch (op.type) { + // These operations should not appear on a properly compiled AST + case "trigger_when_all_completed": + case "trigger_when_first_completed": + case "trigger_on_signal": + case "trigger_when_all_true": + throw new Error(`Invalid AST Operation: Operation (type:${op.type}) should now be present on a properly compiled AST.`); + + + // Nothing to canonicalize + case "wait_for_monitor": + case "flow_last_value": + case "jump_to_position": + case "jump_to_block": + case "trigger_when_first_completed": + break; + + // Cannonicalize args and contents, but don't sort + case "control_wait": + case "control_wait_for_next_value": + case "control_if_else": + case "control_repeat": + case "operator_modulo": + case "operator_add": + case "logging_add_log": + case "operator_lt": + case "operator_gt": + case "data_setvariableto": + case "data_variable": + case "on_data_variable_update": + case "data_lengthoflist": + case "flow_get_thread_id": + case "data_deleteoflist": + case "data_addtolist": + case "data_ui_block_value": + case "op_preload_getter": + case "op_on_block_run": + if (op.args) { + op.args = (op.args as CompiledBlockArgList).map(arg => canonicalize_arg(arg)); + } + if (op.contents) { + op.contents = op.contents.map(content => canonicalize_content(content)); + } + break; + + // Special argument handling + case "command_call_service": + const values = (op.args as CompiledBlockArgCallServiceDict).service_call_values; + if (values) { + (op.args as CompiledBlockArgCallServiceDict).service_call_values = values.map(arg => canonicalize_arg(arg)); + } + break; + + // Canonicalize args and allow sorting them + case "operator_and": + case "operator_equals": + const args = (op.args as CompiledBlockArgList).map(arg => canonicalize_arg(arg)); + + // This is very inefficient, but as canonicalization only makes + // sense on unit tests it might be acceptable + op.args = args.sort((a, b) => stable_stringify(a).localeCompare(stable_stringify(b))); + break; + + // Canonicalize contents and allow sorting them + case "op_fork_execution": + op.contents = op.contents.map(content => canonicalize_content(content)); + op.contents = op.contents.sort((a, b) => stable_stringify(a).localeCompare(stable_stringify(b))); + + // Remove redundant parameters + if (op.args && Array.isArray(op.args)) { + op.args = op.args.filter(a => !(a.type === 'constant' && a.value === 'exit-when-all-completed')) + } + + break; + + default: + if (op.type.startsWith('services.')) { + if (op.args && Array.isArray(op.args)) { + op.args = (op.args as CompiledBlockArgList).map(arg => canonicalize_arg(arg)); + } + if (op.contents) { + op.contents = op.contents.map(content => canonicalize_content(content)); + } + } + else { + console.warn(`Unknown operation: ${op.type}`); + } + } + + delete op.report_state; + + return op; +} + +function canonicalize_ast(ast: CompiledFlowGraph): CompiledFlowGraph { + return ast.map(op => canonicalize_op(op)); +} + +export function canonicalize_ast_list(asts: CompiledFlowGraph[]): CompiledFlowGraph[] { + // This sorting is very inefficient, but as canonicalization only makes + // sense on unit tests it might be acceptable + return asts.map(ast => canonicalize_ast(ast)).sort((a, b) => stable_stringify(a).localeCompare(stable_stringify(b))); +} diff --git a/frontend/src/app/tests/logic/scaffolding/utils.ts b/frontend/src/app/tests/logic/scaffolding/utils.ts new file mode 100644 index 00000000..9ac77257 --- /dev/null +++ b/frontend/src/app/tests/logic/scaffolding/utils.ts @@ -0,0 +1,74 @@ +import { is_pulse_output } from '../../../flow-editor/graph_transformations'; +import { AtomicFlowBlockData, BLOCK_TYPE as ATOMIC_BLOCK_TYPE } from '../../../flow-editor/atomic_flow_block'; +import { BLOCK_TYPE as VALUE_BLOCK_TYPE, DirectValueFlowBlockData } from '../../../flow-editor/direct_value'; +import { BLOCK_TYPE as ENUM_BLOCK_TYPE, EnumDirectValueFlowBlockData } from '../../../flow-editor/enum_direct_value'; +import { FlowGraph } from '../../../flow-editor/flow_graph'; +import { isUiFlowBlockData } from '../../../flow-editor/ui-blocks/ui_flow_block'; + +export function convert_to_graphviz(graph: FlowGraph): string { + const tokens: string[] = ['digraph {']; + const raws: {[key: string]: string} = {} + + let next_raw_id = 100000; + let raw_value_prefixes = '__RAW_VALUE_'; + + for (const node_id of Object.keys(graph.nodes)) { + const node = graph.nodes[node_id]; + + if (node.data.type === ATOMIC_BLOCK_TYPE) { + const a_node = node.data as AtomicFlowBlockData; + + let fillcolor = "#ffffff"; + let fontcolor = "#000000"; + switch (a_node.value.options.type) { + case 'operation': + fillcolor = '#aaaaff'; + break; + case 'getter': + fillcolor = '#aaffaa'; + break; + case 'trigger': + fillcolor = '#ffffaa'; + break; + } + + tokens.push(` "${node_id}"[label="${a_node.value.options.block_function}",` + +`style="filled",shape=rect,fontcolor="${fontcolor}",fillcolor="${fillcolor}"]`); + } + else if (node.data.type === VALUE_BLOCK_TYPE ) { + const v_node = node.data as DirectValueFlowBlockData; + if (typeof v_node.value.value === 'string') { + raws[node_id] = `\\"${v_node.value.value}\\"`; + } + else { + raws[node_id] = v_node.value.value; + } + } + else if (node.data.type === ENUM_BLOCK_TYPE ) { + const e_node = node.data as EnumDirectValueFlowBlockData; + raws[node_id] = e_node.value.value_text; + } + else if (isUiFlowBlockData(node.data)) { + tokens.push(` "${node_id}"[label="${node.data.value.options.id}",` + +`style="filled",shape=rect,fontcolor="#ffaa00",fillcolor="#000000"]`); + } + } + + for (const conn of graph.edges) { + let from_id = conn.from.id; + let extras = ''; + if (raws[from_id]) { + const value = raws[from_id]; + + from_id = raw_value_prefixes + next_raw_id++; + tokens.push(` "${from_id}"[label="${value}"]`); + } + else if (is_pulse_output(graph.nodes[from_id], conn.from.output_index)) { + extras = ',color="black:#ffaa00:black",arrowhead="vee",penwidth=2'; + } + tokens.push(` "${from_id}" -> "${conn.to.id}"[label="${conn.from.output_index} → ${conn.to.input_index}"${extras}];`); + } + + tokens.push('}'); + return tokens.join('\n'); +} diff --git a/frontend/src/app/tests/logic/spreadsheet-compilation/.gitignore b/frontend/src/app/tests/logic/spreadsheet-compilation/.gitignore new file mode 100644 index 00000000..b38f8050 --- /dev/null +++ b/frontend/src/app/tests/logic/spreadsheet-compilation/.gitignore @@ -0,0 +1,2 @@ +*.dot +*.png diff --git a/frontend/src/app/tests/logic/spreadsheet-compilation/01_sample_spreadsheet.spec.ts b/frontend/src/app/tests/logic/spreadsheet-compilation/01_sample_spreadsheet.spec.ts new file mode 100644 index 00000000..7790d54b --- /dev/null +++ b/frontend/src/app/tests/logic/spreadsheet-compilation/01_sample_spreadsheet.spec.ts @@ -0,0 +1,105 @@ +import { compile_spreadsheet } from "../../../program-editors/spreadsheet-editor/spreadsheet-compiler"; +import { ISpreadsheetToolbox } from "../../../program-editors/spreadsheet-editor/spreadsheet-toolbox"; +import { are_equivalent_ast } from "../flow-graph-analysis/utils.spec"; +import { gen_compiled } from "../scaffolding/graph-analysis-tools"; +import { dsl_to_ast } from "../scaffolding/graph-analysis-tools-ast-dsl"; +import { FakeSpreadsheetToolbox } from './fake-toolbox'; + +const BRIDGE_ID = '7e90d22c-4518-4e93-9106-e69e4a85bc34'; +const BRIDGE_NAME = "matrix"; + +export function gen_sheet(): {[key: string]: string} { + return { + "C2":"=7e34_on_new_message()", + "E2": "=7e34_answer_message(\"Hello\" + C2 + \" \" + 123, 7e34_get_value(123 + 456))", + } +} + +export function gen_toolbox() : ISpreadsheetToolbox { + return new FakeSpreadsheetToolbox([ + { + id: BRIDGE_ID, + name: BRIDGE_NAME, + blocks: [ + { + id: '7e34_on_new_message', + message: 'When I say something in any channel. Set %1', + }, + { + id: '7e34_answer_message', + message: 'Respond %1', + }, + { + id: '7e34_get_value', + message: 'Get value', + }, + ] + } + ], [ + { + "subkey": null, + "save_to": { + "type": "argument", + "index": 0 + }, + "message": "When I say something in any channel. Set %1", + "key": "on_new_message", + "function_name": "on_new_message", + "service_port_id": BRIDGE_ID, + "id": 'services.' + BRIDGE_ID + '.' + "on_new_message", + "block_type": "trigger", + block_result_type: null, + "block_id": "on_new_message", + "arguments": [ + { + "var_type": "string", + "type": "variable", + "default_value": "undefined", + "class": "single" + } + ] + }, + { + "save_to": null, + "message": "Respond %1", + "function_name": "answer_message", + "block_type": "operation", + "block_result_type": null, + "id": 'services.' + BRIDGE_ID + '.' + "answer_message", + "service_port_id": BRIDGE_ID, + "block_id": "answer_message", + "arguments": [ + { + "type": "string", + "default_value": "Hello", + } + ] + }, + { + "save_to": null, + "message": "Get value", + "function_name": "get_value", + "block_type": "getter", + "id": 'services.' + BRIDGE_ID + '.' + "get_value", + "service_port_id": BRIDGE_ID, + "block_result_type": "string", + "block_id": "get_value", + "arguments": [], + } + ]); +} + +describe('Spreadsheet-01: Sample spreadsheet.', () => { + it('Should be able to compile', async () => { + are_equivalent_ast(compile_spreadsheet(gen_sheet(), gen_toolbox()), [ + gen_compiled(dsl_to_ast( + `;PM-DSL ;; Entrypoint for mmm-mode + (services.${BRIDGE_ID}.on_new_message) + (call-service id: ${BRIDGE_ID} + action: answer_message + values: ( (+ (+ (+ "Hello" (flow-last-value "C2" 1)) " ") 123) + (call-service id: ${BRIDGE_ID} action: get_value values: ((+ 123 456))))) + `)) + ]); + }); +}); diff --git a/frontend/src/app/tests/logic/spreadsheet-compilation/_gen_graphs_and_tables.utils.ts b/frontend/src/app/tests/logic/spreadsheet-compilation/_gen_graphs_and_tables.utils.ts new file mode 100644 index 00000000..4ba963db --- /dev/null +++ b/frontend/src/app/tests/logic/spreadsheet-compilation/_gen_graphs_and_tables.utils.ts @@ -0,0 +1,41 @@ +import { build_graph } from "../../../program-editors/spreadsheet-editor/spreadsheet-compiler"; +import { spawn } from 'child_process'; +import * as fs from 'fs'; +import { convert_to_graphviz } from '../scaffolding/utils'; +import * as util from 'util'; + +export function run(): Promise { + const files = fs.readdirSync(__dirname); + const candidates = files.filter((file: string) => file.match(/\d+.*.spec.ts$/)); + + let done_count = 0; + const promises = candidates.map((file: string) => { + return new Promise(async (resolve, reject) => { + try { + const mod_name = file.substr(0, file.length - 3); + const mod = require('./' + mod_name); // Remove '.ts' + if (mod.gen_sheet) { + const sheet = mod.gen_sheet(); + const flow = build_graph(sheet, mod.gen_toolbox()) + + await util.promisify(fs.writeFile)(`${__dirname}/${file}.dot`, convert_to_graphviz(flow)); + spawn("dot", ["-Tpng", `${__dirname}/${file}.dot`, '-o', `${__dirname}/${file}.png`]); + + done_count++; + process.stdout.write(`[${done_count}/${candidates.length}] ${file}\n`) + resolve(file); + } + else { + process.stdout.write('No sheet found\n'); + resolve(null); + } + } + catch (err) { + process.stdout.write(file + ': ' + err.toString() + '\n'); + reject(err); + } + }); + }); + + return Promise.all(promises) as Promise; +} diff --git a/frontend/src/app/tests/logic/spreadsheet-compilation/fake-toolbox.ts b/frontend/src/app/tests/logic/spreadsheet-compilation/fake-toolbox.ts new file mode 100644 index 00000000..5f964e82 --- /dev/null +++ b/frontend/src/app/tests/logic/spreadsheet-compilation/fake-toolbox.ts @@ -0,0 +1,21 @@ +import { ISpreadsheetToolbox, CategoryDef, simplify_id } from "../../../program-editors/spreadsheet-editor/spreadsheet-toolbox"; +import { ResolvedCustomBlock } from "../../../custom_block"; + +export class FakeSpreadsheetToolbox implements ISpreadsheetToolbox { + public categories: CategoryDef[]; + public nonEmptyCategories: CategoryDef[]; + public blockMap: { [key: string]: { cat: CategoryDef; block: ResolvedCustomBlock; }; }; + + constructor (categories: CategoryDef[], blocks: ResolvedCustomBlock[]) { + this.categories = categories; + this.nonEmptyCategories = categories.filter(c => c.blocks.length > 0); + this.blockMap = {}; + + for (const block of blocks) { + this.blockMap[simplify_id(block)] = { + cat: categories.find(c => c.id === block.service_port_id), + block: block + }; + } + } +} diff --git a/frontend/src/app/types/collaborator.ts b/frontend/src/app/types/collaborator.ts new file mode 100644 index 00000000..1c5b7df4 --- /dev/null +++ b/frontend/src/app/types/collaborator.ts @@ -0,0 +1,21 @@ +export type CollaboratorRole = 'admin' | 'editor' | 'viewer'; + +export function roleToIcon(role: CollaboratorRole) { + switch(role) { + case 'admin': + return 'flash_on'; + case 'editor': + return 'brush'; + case 'viewer': + return 'visibility'; + default: + return null; + } +} + +export interface Collaborator { + id: string; + username: string; + picture?: string; + role: CollaboratorRole +}; diff --git a/frontend/src/app/ui/progbar.ts b/frontend/src/app/ui/progbar.ts index a9eb5ab7..5c1720d6 100644 --- a/frontend/src/app/ui/progbar.ts +++ b/frontend/src/app/ui/progbar.ts @@ -1,4 +1,4 @@ -declare const NProgress; +declare const NProgress : any; export function track(p: Promise): Promise { NProgress.configure({ showSpinner: false }); diff --git a/frontend/src/app/user.ts b/frontend/src/app/user.ts new file mode 100644 index 00000000..79a89452 --- /dev/null +++ b/frontend/src/app/user.ts @@ -0,0 +1,11 @@ +export interface UserTags { + is_admin: boolean; + is_advanced: boolean; + is_in_preview: boolean; +} + +export interface User { + username: string; + user_id: string; + tags: UserTags; +} diff --git a/frontend/src/app/utils.ts b/frontend/src/app/utils.ts new file mode 100644 index 00000000..a8c45200 --- /dev/null +++ b/frontend/src/app/utils.ts @@ -0,0 +1,114 @@ +import { IconReference, HashedIcon } from './connection'; +import { EnvironmentService } from './environment.service'; + +function toWebsocketUrl(env: EnvironmentService, url: string): string { + let baseServerPath = document.location.origin; + const apiHost = env.getBrowserApiHost(); + if (apiHost != '') { + baseServerPath = apiHost; + } + + if (url.startsWith('/')) { // We need an absolute address for this + url = baseServerPath + url; + } + return url.replace(/^http/, 'ws'); +} + + +function getUserPictureUrl(env: EnvironmentService, userId: string): string { + return `${env.getBrowserApiRoot()}/users/by-id/${userId}/picture`; +} + +function getGroupPictureUrl(env: EnvironmentService, groupId: string): string { + return `${env.getBrowserApiRoot()}/groups/by-id/${groupId}/picture`; +} + +function iconDataToUrl(env: EnvironmentService, icon: IconReference, bridge_id: string): string | undefined { + if (!icon) { return undefined; } + if ((icon as {url: string}).url) { + return (icon as {url: string}).url; + } + else if ((icon as HashedIcon).sha256) { + return env.getBrowserApiRoot() + '/assets/icons/' + bridge_id; + } +} + +function unixMsToStr(ms_timestamp: number, options?: { ms_precision?: boolean }): string { + const date = new Date(ms_timestamp); + + if (!options) { + options = {}; + } + + const left_pad = ((val: string | number, target_length: number, pad_character: string) => { + let str = val.toString(); + + while (str.length < target_length) { + str = pad_character + str; + } + return str; + }); + const pad02 = (val: string|number) => { + return left_pad(val, 2, '0'); + } + + let result = (`${date.getFullYear()}/${pad02(date.getMonth() + 1)}/${pad02(date.getDate())} ` + + ` - ${pad02(date.getHours())}:${pad02(date.getMinutes())}:${pad02(date.getSeconds())}`); + + if (options.ms_precision) { + result += `.${date.getMilliseconds()}`; + } + return result; +} + +function addTokenQueryString(url: string, token: string): string { + if (url.indexOf('?') === -1) { + return url + '?token=' + token; + } + else { + return url + '&token=' + token; + } +} + +function ground(environmentService: EnvironmentService, obj: any, field: string){ + if (obj[field] && obj[field].startsWith('/')) { + obj[field] = environmentService.getApiRoot() + obj[field]; + } + + return obj; +} + +function manageTopLevelError(f: (input: I) => O): (input: I) => O { + return (...args) => { + try { + return f(...args); + } + catch (error) { + logError(error); + } + }; +} + +function logError(error: Error & { _logged?: boolean }) { + if (error._logged) { + return; + } + + error._logged = true; + + console.error(error); +} + + +export { + toWebsocketUrl, + getUserPictureUrl, + getGroupPictureUrl, + iconDataToUrl, + unixMsToStr, + addTokenQueryString, + ground, + + manageTopLevelError, + logError, +}; diff --git a/frontend/src/assets/.gitignore b/frontend/src/assets/.gitignore new file mode 100644 index 00000000..e2d7ab75 --- /dev/null +++ b/frontend/src/assets/.gitignore @@ -0,0 +1 @@ +flow_editor.css diff --git a/frontend/src/assets/blocks-icon.svg b/frontend/src/assets/blocks-icon.svg new file mode 100644 index 00000000..b98ba5d6 --- /dev/null +++ b/frontend/src/assets/blocks-icon.svg @@ -0,0 +1,21 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/cursor.svg b/frontend/src/assets/cursor.svg new file mode 100644 index 00000000..67019767 --- /dev/null +++ b/frontend/src/assets/cursor.svg @@ -0,0 +1,2 @@ + + diff --git a/frontend/src/assets/flow-icon.svg b/frontend/src/assets/flow-icon.svg new file mode 100644 index 00000000..fca63449 --- /dev/null +++ b/frontend/src/assets/flow-icon.svg @@ -0,0 +1,19 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/frontend/src/assets/icons/aemet_logo.jpg b/frontend/src/assets/icons/aemet_logo.jpg new file mode 100644 index 00000000..e5eb0a9c Binary files /dev/null and b/frontend/src/assets/icons/aemet_logo.jpg differ diff --git a/frontend/src/assets/icons/aemet_logo.png b/frontend/src/assets/icons/aemet_logo.png new file mode 100644 index 00000000..5f71e36c Binary files /dev/null and b/frontend/src/assets/icons/aemet_logo.png differ diff --git a/frontend/src/assets/icons/btn_google_signin_dark_normal_web.png b/frontend/src/assets/icons/btn_google_signin_dark_normal_web.png new file mode 100644 index 00000000..b1327b4f Binary files /dev/null and b/frontend/src/assets/icons/btn_google_signin_dark_normal_web.png differ diff --git a/frontend/src/assets/icons/clone.svg b/frontend/src/assets/icons/clone.svg new file mode 100644 index 00000000..d939a100 --- /dev/null +++ b/frontend/src/assets/icons/clone.svg @@ -0,0 +1,103 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/icons/format_color_text.svg b/frontend/src/assets/icons/format_color_text.svg new file mode 100644 index 00000000..8fc36297 --- /dev/null +++ b/frontend/src/assets/icons/format_color_text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/insert_link.svg b/frontend/src/assets/icons/insert_link.svg new file mode 100644 index 00000000..50ce54c1 --- /dev/null +++ b/frontend/src/assets/icons/insert_link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/no-image.svg b/frontend/src/assets/icons/no-image.svg new file mode 100644 index 00000000..8a259cd7 --- /dev/null +++ b/frontend/src/assets/icons/no-image.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/icons/settings.svg b/frontend/src/assets/icons/settings.svg new file mode 100644 index 00000000..23f7e77a --- /dev/null +++ b/frontend/src/assets/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/telegram_logo.png b/frontend/src/assets/icons/telegram_logo.png similarity index 100% rename from frontend/src/assets/telegram_logo.png rename to frontend/src/assets/icons/telegram_logo.png diff --git a/frontend/src/assets/logo-dark.png b/frontend/src/assets/logo-dark.png new file mode 100644 index 00000000..0f8bdfae Binary files /dev/null and b/frontend/src/assets/logo-dark.png differ diff --git a/frontend/src/assets/logo.xcf b/frontend/src/assets/logo.xcf index 5f433b51..86bcae4b 100644 Binary files a/frontend/src/assets/logo.xcf and b/frontend/src/assets/logo.xcf differ diff --git a/frontend/src/assets/no-group-picture.svg b/frontend/src/assets/no-group-picture.svg new file mode 100644 index 00000000..30ef7340 --- /dev/null +++ b/frontend/src/assets/no-group-picture.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/frontend/src/assets/profile-not-set.png b/frontend/src/assets/profile-not-set.png new file mode 100644 index 00000000..1db01dab Binary files /dev/null and b/frontend/src/assets/profile-not-set.png differ diff --git a/frontend/src/assets/showcase/flow/flow-example.png b/frontend/src/assets/showcase/flow/flow-example.png new file mode 100644 index 00000000..024ec315 Binary files /dev/null and b/frontend/src/assets/showcase/flow/flow-example.png differ diff --git a/frontend/src/assets/showcase/scratch/scratch-example.png b/frontend/src/assets/showcase/scratch/scratch-example.png new file mode 100644 index 00000000..9833a388 Binary files /dev/null and b/frontend/src/assets/showcase/scratch/scratch-example.png differ diff --git a/frontend/src/assets/spreadsheet-icon.svg b/frontend/src/assets/spreadsheet-icon.svg new file mode 100644 index 00000000..88396500 --- /dev/null +++ b/frontend/src/assets/spreadsheet-icon.svg @@ -0,0 +1,118 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/sprites/delete_forever-black.svg b/frontend/src/assets/sprites/delete_forever-black.svg new file mode 100644 index 00000000..8b57e2bc --- /dev/null +++ b/frontend/src/assets/sprites/delete_forever-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/sprites/expand_more.svg b/frontend/src/assets/sprites/expand_more.svg new file mode 100644 index 00000000..8a835b75 --- /dev/null +++ b/frontend/src/assets/sprites/expand_more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/timezones.json b/frontend/src/assets/timezones.json new file mode 100644 index 00000000..f743d241 --- /dev/null +++ b/frontend/src/assets/timezones.json @@ -0,0 +1 @@ +[{"country":"CI","latlong":"+0519−00402","tz":"Africa/Abidjan","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"GH","latlong":"+0533−00013","tz":"Africa/Accra","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"ET","latlong":"+0902+03842","tz":"Africa/Addis_Ababa","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"DZ","latlong":"+3647+00303","tz":"Africa/Algiers","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+01:00","notes":""},{"country":"ER","latlong":"+1520+03853","tz":"Africa/Asmara","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"ML","latlong":"+1239−00800","tz":"Africa/Bamako","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"CF","latlong":"+0422+01835","tz":"Africa/Bangui","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"GM","latlong":"+1328−01639","tz":"Africa/Banjul","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"GW","latlong":"+1151−01535","tz":"Africa/Bissau","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"MW","latlong":"−1547+03500","tz":"Africa/Blantyre","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Maputo"},{"country":"CG","latlong":"−0416+01517","tz":"Africa/Brazzaville","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"BI","latlong":"−0323+02922","tz":"Africa/Bujumbura","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Maputo"},{"country":"EG","latlong":"+3003+03115","tz":"Africa/Cairo","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"MA","latlong":"+3339−00735","tz":"Africa/Casablanca","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+01:00","notes":""},{"country":"ES","latlong":"+3553−00519","tz":"Africa/Ceuta","region":"Ceuta, Melilla","status":"Canonical","offset":"+01:00","dst_offset":"+01:00","notes":""},{"country":"GN","latlong":"+0931−01343","tz":"Africa/Conakry","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"SN","latlong":"+1440−01726","tz":"Africa/Dakar","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"TZ","latlong":"−0648+03917","tz":"Africa/Dar_es_Salaam","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"DJ","latlong":"+1136+04309","tz":"Africa/Djibouti","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"CM","latlong":"+0403+00942","tz":"Africa/Douala","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"EH","latlong":"+2709−01312","tz":"Africa/El_Aaiun","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+01:00","notes":""},{"country":"SL","latlong":"+0830−01315","tz":"Africa/Freetown","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"BW","latlong":"−2439+02555","tz":"Africa/Gaborone","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Maputo"},{"country":"ZW","latlong":"−1750+03103","tz":"Africa/Harare","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Maputo"},{"country":"ZA","latlong":"−2615+02800","tz":"Africa/Johannesburg","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"SS","latlong":"+0451+03136","tz":"Africa/Juba","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"UG","latlong":"+0019+03225","tz":"Africa/Kampala","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"SD","latlong":"+1536+03232","tz":"Africa/Khartoum","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"RW","latlong":"−0157+03004","tz":"Africa/Kigali","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Maputo"},{"country":"CD","latlong":"−0418+01518","tz":"Africa/Kinshasa","region":"Dem. Rep. of Congo (west)","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"NG","latlong":"+0627+00324","tz":"Africa/Lagos","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+01:00","notes":""},{"country":"GA","latlong":"+0023+00927","tz":"Africa/Libreville","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"TG","latlong":"+0608+00113","tz":"Africa/Lome","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"AO","latlong":"−0848+01314","tz":"Africa/Luanda","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"CD","latlong":"−1140+02728","tz":"Africa/Lubumbashi","region":"Dem. Rep. of Congo (east)","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Maputo"},{"country":"ZM","latlong":"−1525+02817","tz":"Africa/Lusaka","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Maputo"},{"country":"GQ","latlong":"+0345+00847","tz":"Africa/Malabo","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"MZ","latlong":"−2558+03235","tz":"Africa/Maputo","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"LS","latlong":"−2928+02730","tz":"Africa/Maseru","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Johannesburg"},{"country":"SZ","latlong":"−2618+03106","tz":"Africa/Mbabane","region":"","status":"Alias","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Johannesburg"},{"country":"SO","latlong":"+0204+04522","tz":"Africa/Mogadishu","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"LR","latlong":"+0618−01047","tz":"Africa/Monrovia","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"KE","latlong":"−0117+03649","tz":"Africa/Nairobi","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"TD","latlong":"+1207+01503","tz":"Africa/Ndjamena","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+01:00","notes":""},{"country":"NE","latlong":"+1331+00207","tz":"Africa/Niamey","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"MR","latlong":"+1806−01557","tz":"Africa/Nouakchott","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"BF","latlong":"+1222−00131","tz":"Africa/Ouagadougou","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"BJ","latlong":"+0629+00237","tz":"Africa/Porto-Novo","region":"","status":"Alias","offset":"+01:00","dst_offset":"+01:00","notes":"Link to Africa/Lagos"},{"country":"ST","latlong":"+0020+00644","tz":"Africa/Sao_Tome","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Lagos"},{"country":"","latlong":"","tz":"Africa/Timbuktu","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"LY","latlong":"+3254+01311","tz":"Africa/Tripoli","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"TN","latlong":"+3648+01011","tz":"Africa/Tunis","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+01:00","notes":""},{"country":"NA","latlong":"−2234+01706","tz":"Africa/Windhoek","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"US","latlong":"+515248−1763929","tz":"America/Adak","region":"Aleutian Islands","status":"Canonical","offset":"−10:00","dst_offset":"−09:00","notes":""},{"country":"US","latlong":"+611305−1495401","tz":"America/Anchorage","region":"Alaska (most areas)","status":"Canonical","offset":"−09:00","dst_offset":"−08:00","notes":""},{"country":"AI","latlong":"+1812−06304","tz":"America/Anguilla","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"AG","latlong":"+1703−06148","tz":"America/Antigua","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"BR","latlong":"−0712−04812","tz":"America/Araguaina","region":"Tocantins","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−3436−05827","tz":"America/Argentina/Buenos_Aires","region":"Buenos Aires (BA, CF)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−2828−06547","tz":"America/Argentina/Catamarca","region":"Catamarca (CT); Chubut (CH)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"","latlong":"","tz":"America/Argentina/ComodRivadavia","region":"","status":"Alias","offset":"−03:00","dst_offset":"−03:00","notes":"Link to America/Argentina/Catamarca"},{"country":"AR","latlong":"−3124−06411","tz":"America/Argentina/Cordoba","region":"Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−2411−06518","tz":"America/Argentina/Jujuy","region":"Jujuy (JY)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−2926−06651","tz":"America/Argentina/La_Rioja","region":"La Rioja (LR)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−3253−06849","tz":"America/Argentina/Mendoza","region":"Mendoza (MZ)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−5138−06913","tz":"America/Argentina/Rio_Gallegos","region":"Santa Cruz (SC)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−2447−06525","tz":"America/Argentina/Salta","region":"Salta (SA, LP, NQ, RN)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−3132−06831","tz":"America/Argentina/San_Juan","region":"San Juan (SJ)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−3319−06621","tz":"America/Argentina/San_Luis","region":"San Luis (SL)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−2649−06513","tz":"America/Argentina/Tucuman","region":"Tucuman (TM)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AR","latlong":"−5448−06818","tz":"America/Argentina/Ushuaia","region":"Tierra del Fuego (TF)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"AW","latlong":"+1230−06958","tz":"America/Aruba","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Curacao"},{"country":"PY","latlong":"−2516−05740","tz":"America/Asuncion","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"CA","latlong":"+484531−0913718","tz":"America/Atikokan","region":"EST - ON (Atikokan); NU (Coral H)","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"","latlong":"","tz":"America/Atka","region":"","status":"Alias","offset":"−10:00","dst_offset":"−09:00","notes":"Link to America/Adak"},{"country":"BR","latlong":"−1259−03831","tz":"America/Bahia","region":"Bahia","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"MX","latlong":"+2048−10515","tz":"America/Bahia_Banderas","region":"Central Time - Bahia de Banderas","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"BB","latlong":"+1306−05937","tz":"America/Barbados","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"BR","latlong":"−0127−04829","tz":"America/Belem","region":"Para (east); Amapa","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"BZ","latlong":"+1730−08812","tz":"America/Belize","region":"","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"CA","latlong":"+5125−05707","tz":"America/Blanc-Sablon","region":"AST - QC (Lower North Shore)","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"BR","latlong":"+0249−06040","tz":"America/Boa_Vista","region":"Roraima","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"CO","latlong":"+0436−07405","tz":"America/Bogota","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+433649−1161209","tz":"America/Boise","region":"Mountain - ID (south); OR (east)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"","latlong":"","tz":"America/Buenos_Aires","region":"","status":"Alias","offset":"−03:00","dst_offset":"−03:00","notes":"Link to America/Argentina/Buenos_Aires"},{"country":"CA","latlong":"+690650−1050310","tz":"America/Cambridge_Bay","region":"Mountain - NU (west)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"BR","latlong":"−2027−05437","tz":"America/Campo_Grande","region":"Mato Grosso do Sul","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"MX","latlong":"+2105−08646","tz":"America/Cancun","region":"Eastern Standard Time - Quintana Roo","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"VE","latlong":"+1030−06656","tz":"America/Caracas","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"","latlong":"","tz":"America/Catamarca","region":"","status":"Alias","offset":"−03:00","dst_offset":"−03:00","notes":"Link to America/Argentina/Catamarca"},{"country":"GF","latlong":"+0456−05220","tz":"America/Cayenne","region":"","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"KY","latlong":"+1918−08123","tz":"America/Cayman","region":"","status":"Alias","offset":"−05:00","dst_offset":"−05:00","notes":"Link to America/Panama"},{"country":"US","latlong":"+4151−08739","tz":"America/Chicago","region":"Central (most areas)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"MX","latlong":"+2838−10605","tz":"America/Chihuahua","region":"Mountain Time - Chihuahua (most areas)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"","latlong":"","tz":"America/Coral_Harbour","region":"","status":"Alias","offset":"−05:00","dst_offset":"−05:00","notes":"Link to America/Atikokan"},{"country":"","latlong":"","tz":"America/Cordoba","region":"","status":"Alias","offset":"−03:00","dst_offset":"−03:00","notes":"Link to America/Argentina/Cordoba"},{"country":"CR","latlong":"+0956−08405","tz":"America/Costa_Rica","region":"","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"CA","latlong":"+4906−11631","tz":"America/Creston","region":"MST - BC (Creston)","status":"Canonical","offset":"−07:00","dst_offset":"−07:00","notes":""},{"country":"BR","latlong":"−1535−05605","tz":"America/Cuiaba","region":"Mato Grosso","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"CW","latlong":"+1211−06900","tz":"America/Curacao","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"GL","latlong":"+7646−01840","tz":"America/Danmarkshavn","region":"National Park (east coast)","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"CA","latlong":"+6404−13925","tz":"America/Dawson","region":"Pacific - Yukon (north)","status":"Canonical","offset":"−08:00","dst_offset":"−07:00","notes":""},{"country":"CA","latlong":"+5946−12014","tz":"America/Dawson_Creek","region":"MST - BC (Dawson Cr, Ft St John)","status":"Canonical","offset":"−07:00","dst_offset":"−07:00","notes":""},{"country":"US","latlong":"+394421−1045903","tz":"America/Denver","region":"Mountain (most areas)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"US","latlong":"+421953−0830245","tz":"America/Detroit","region":"Eastern - MI (most areas)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"DM","latlong":"+1518−06124","tz":"America/Dominica","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"CA","latlong":"+5333−11328","tz":"America/Edmonton","region":"Mountain - AB; BC (E); SK (W)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"BR","latlong":"−0640−06952","tz":"America/Eirunepe","region":"Amazonas (west)","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"SV","latlong":"+1342−08912","tz":"America/El_Salvador","region":"","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"","latlong":"","tz":"America/Ensenada","region":"","status":"Alias","offset":"−08:00","dst_offset":"−07:00","notes":"Link to America/Tijuana"},{"country":"CA","latlong":"+5848−12242","tz":"America/Fort_Nelson","region":"MST - BC (Ft Nelson)","status":"Canonical","offset":"−07:00","dst_offset":"−07:00","notes":""},{"country":"","latlong":"","tz":"America/Fort_Wayne","region":"","status":"Alias","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Indiana/Indianapolis"},{"country":"BR","latlong":"−0343−03830","tz":"America/Fortaleza","region":"Brazil (northeast: MA, PI, CE, RN, PB)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"CA","latlong":"+4612−05957","tz":"America/Glace_Bay","region":"Atlantic - NS (Cape Breton)","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"GL","latlong":"+6411−05144","tz":"America/Godthab","region":"Greenland (most areas)","status":"Canonical","offset":"−03:00","dst_offset":"−02:00","notes":""},{"country":"CA","latlong":"+5320−06025","tz":"America/Goose_Bay","region":"Atlantic - Labrador (most areas)","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"TC","latlong":"+2128−07108","tz":"America/Grand_Turk","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"GD","latlong":"+1203−06145","tz":"America/Grenada","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"GP","latlong":"+1614−06132","tz":"America/Guadeloupe","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"GT","latlong":"+1438−09031","tz":"America/Guatemala","region":"","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"EC","latlong":"−0210−07950","tz":"America/Guayaquil","region":"Ecuador (mainland)","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"GY","latlong":"+0648−05810","tz":"America/Guyana","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"CA","latlong":"+4439−06336","tz":"America/Halifax","region":"Atlantic - NS (most areas); PE","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"CU","latlong":"+2308−08222","tz":"America/Havana","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"MX","latlong":"+2904−11058","tz":"America/Hermosillo","region":"Mountain Standard Time - Sonora","status":"Canonical","offset":"−07:00","dst_offset":"−07:00","notes":""},{"country":"US","latlong":"+394606−0860929","tz":"America/Indiana/Indianapolis","region":"Eastern - IN (most areas)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+411745−0863730","tz":"America/Indiana/Knox","region":"Central - IN (Starke)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+382232−0862041","tz":"America/Indiana/Marengo","region":"Eastern - IN (Crawford)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+382931−0871643","tz":"America/Indiana/Petersburg","region":"Eastern - IN (Pike)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+375711−0864541","tz":"America/Indiana/Tell_City","region":"Central - IN (Perry)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+384452−0850402","tz":"America/Indiana/Vevay","region":"Eastern - IN (Switzerland)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+384038−0873143","tz":"America/Indiana/Vincennes","region":"Eastern - IN (Da, Du, K, Mn)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+410305−0863611","tz":"America/Indiana/Winamac","region":"Eastern - IN (Pulaski)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"","latlong":"","tz":"America/Indianapolis","region":"","status":"Alias","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Indiana/Indianapolis"},{"country":"CA","latlong":"+682059−13343","tz":"America/Inuvik","region":"Mountain - NT (west)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"CA","latlong":"+6344−06828","tz":"America/Iqaluit","region":"Eastern - NU (most east areas)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"JM","latlong":"+175805−0764736","tz":"America/Jamaica","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"","latlong":"","tz":"America/Jujuy","region":"","status":"Alias","offset":"−03:00","dst_offset":"−03:00","notes":"Link to America/Argentina/Jujuy"},{"country":"US","latlong":"+581807−1342511","tz":"America/Juneau","region":"Alaska - Juneau area","status":"Canonical","offset":"−09:00","dst_offset":"−08:00","notes":""},{"country":"US","latlong":"+381515−0854534","tz":"America/Kentucky/Louisville","region":"Eastern - KY (Louisville area)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+364947−0845057","tz":"America/Kentucky/Monticello","region":"Eastern - KY (Wayne)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"","latlong":"","tz":"America/Knox_IN","region":"","status":"Alias","offset":"−06:00","dst_offset":"−05:00","notes":"Link to America/Indiana/Knox"},{"country":"BQ","latlong":"+120903−0681636","tz":"America/Kralendijk","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Curacao"},{"country":"BO","latlong":"−1630−06809","tz":"America/La_Paz","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"PE","latlong":"−1203−07703","tz":"America/Lima","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+340308−1181434","tz":"America/Los_Angeles","region":"Pacific","status":"Canonical","offset":"−08:00","dst_offset":"−07:00","notes":""},{"country":"","latlong":"","tz":"America/Louisville","region":"","status":"Alias","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Kentucky/Louisville"},{"country":"SX","latlong":"+180305−0630250","tz":"America/Lower_Princes","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Curacao"},{"country":"BR","latlong":"−0940−03543","tz":"America/Maceio","region":"Alagoas, Sergipe","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"NI","latlong":"+1209−08617","tz":"America/Managua","region":"","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"BR","latlong":"−0308−06001","tz":"America/Manaus","region":"Amazonas (east)","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"MF","latlong":"+1804−06305","tz":"America/Marigot","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"MQ","latlong":"+1436−06105","tz":"America/Martinique","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"MX","latlong":"+2550−09730","tz":"America/Matamoros","region":"Central Time US - Coahuila, Nuevo Leon, Tamaulipas (US border)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"MX","latlong":"+2313−10625","tz":"America/Mazatlan","region":"Mountain Time - Baja California Sur, Nayarit, Sinaloa","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"","latlong":"","tz":"America/Mendoza","region":"","status":"Alias","offset":"−03:00","dst_offset":"−03:00","notes":"Link to America/Argentina/Mendoza"},{"country":"US","latlong":"+450628−0873651","tz":"America/Menominee","region":"Central - MI (Wisconsin border)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"MX","latlong":"+2058−08937","tz":"America/Merida","region":"Central Time - Campeche, Yucatan","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+550737−1313435","tz":"America/Metlakatla","region":"Alaska - Annette Island","status":"Canonical","offset":"−09:00","dst_offset":"−08:00","notes":""},{"country":"MX","latlong":"+1924−09909","tz":"America/Mexico_City","region":"Central Time","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"PM","latlong":"+4703−05620","tz":"America/Miquelon","region":"","status":"Canonical","offset":"−03:00","dst_offset":"−02:00","notes":""},{"country":"CA","latlong":"+4606−06447","tz":"America/Moncton","region":"Atlantic - New Brunswick","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"MX","latlong":"+2540−10019","tz":"America/Monterrey","region":"Central Time - Durango; Coahuila, Nuevo Leon, Tamaulipas (most areas)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"UY","latlong":"−3453−05611","tz":"America/Montevideo","region":"","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"","latlong":"","tz":"America/Montreal","region":"","status":"Alias","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Toronto"},{"country":"MS","latlong":"+1643−06213","tz":"America/Montserrat","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"BS","latlong":"+2505−07721","tz":"America/Nassau","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+404251−0740023","tz":"America/New_York","region":"Eastern (most areas)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"CA","latlong":"+4901−08816","tz":"America/Nipigon","region":"Eastern - ON, QC (no DST 1967−73)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"US","latlong":"+643004−1652423","tz":"America/Nome","region":"Alaska (west)","status":"Canonical","offset":"−09:00","dst_offset":"−08:00","notes":""},{"country":"BR","latlong":"−0351−03225","tz":"America/Noronha","region":"Atlantic islands","status":"Canonical","offset":"−02:00","dst_offset":"−02:00","notes":""},{"country":"US","latlong":"+471551−1014640","tz":"America/North_Dakota/Beulah","region":"Central - ND (Mercer)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+470659−1011757","tz":"America/North_Dakota/Center","region":"Central - ND (Oliver)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+465042−1012439","tz":"America/North_Dakota/New_Salem","region":"Central - ND (Morton rural)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"MX","latlong":"+2934−10425","tz":"America/Ojinaga","region":"Mountain Time US - Chihuahua (US border)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"PA","latlong":"+0858−07932","tz":"America/Panama","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"CA","latlong":"+6608−06544","tz":"America/Pangnirtung","region":"Eastern - NU (Pangnirtung)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"SR","latlong":"+0550−05510","tz":"America/Paramaribo","region":"","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"US","latlong":"+332654−1120424","tz":"America/Phoenix","region":"MST - Arizona (except Navajo)","status":"Canonical","offset":"−07:00","dst_offset":"−07:00","notes":""},{"country":"TT","latlong":"+1039−06131","tz":"America/Port_of_Spain","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"HT","latlong":"+1832−07220","tz":"America/Port-au-Prince","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"","latlong":"","tz":"America/Porto_Acre","region":"","status":"Alias","offset":"−05:00","dst_offset":"−05:00","notes":"Link to America/Rio_Branco"},{"country":"BR","latlong":"−0846−06354","tz":"America/Porto_Velho","region":"Rondonia","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"PR","latlong":"+182806−0660622","tz":"America/Puerto_Rico","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"CL","latlong":"−5309−07055","tz":"America/Punta_Arenas","region":"Region of Magallanes","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":"Magallanes Region"},{"country":"CA","latlong":"+4843−09434","tz":"America/Rainy_River","region":"Central - ON (Rainy R, Ft Frances)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"CA","latlong":"+6249−0920459","tz":"America/Rankin_Inlet","region":"Central - NU (central)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"BR","latlong":"−0803−03454","tz":"America/Recife","region":"Pernambuco","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"CA","latlong":"+5024−10439","tz":"America/Regina","region":"CST - SK (most areas)","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"CA","latlong":"+744144−0944945","tz":"America/Resolute","region":"Central - NU (Resolute)","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"BR","latlong":"−0958−06748","tz":"America/Rio_Branco","region":"Acre","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":""},{"country":"","latlong":"","tz":"America/Rosario","region":"","status":"Alias","offset":"−03:00","dst_offset":"−03:00","notes":"Link to America/Argentina/Cordoba"},{"country":"","latlong":"","tz":"America/Santa_Isabel","region":"","status":"Alias","offset":"−08:00","dst_offset":"−07:00","notes":"Link to America/Tijuana"},{"country":"BR","latlong":"−0226−05452","tz":"America/Santarem","region":"Para (west)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"CL","latlong":"−3327−07040","tz":"America/Santiago","region":"Chile (most areas)","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"DO","latlong":"+1828−06954","tz":"America/Santo_Domingo","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":""},{"country":"BR","latlong":"−2332−04637","tz":"America/Sao_Paulo","region":"Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS)","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"GL","latlong":"+7029−02158","tz":"America/Scoresbysund","region":"Scoresbysund/Ittoqqortoormiit","status":"Canonical","offset":"−01:00","dst_offset":"+00:00","notes":""},{"country":"","latlong":"","tz":"America/Shiprock","region":"","status":"Alias","offset":"−07:00","dst_offset":"−06:00","notes":"Link to America/Denver"},{"country":"US","latlong":"+571035−1351807","tz":"America/Sitka","region":"Alaska - Sitka area","status":"Canonical","offset":"−09:00","dst_offset":"−08:00","notes":""},{"country":"BL","latlong":"+1753−06251","tz":"America/St_Barthelemy","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"CA","latlong":"+4734−05243","tz":"America/St_Johns","region":"Newfoundland; Labrador (southeast)","status":"Canonical","offset":"−03:30","dst_offset":"−02:30","notes":""},{"country":"KN","latlong":"+1718−06243","tz":"America/St_Kitts","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"LC","latlong":"+1401−06100","tz":"America/St_Lucia","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"VI","latlong":"+1821−06456","tz":"America/St_Thomas","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"VC","latlong":"+1309−06114","tz":"America/St_Vincent","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"CA","latlong":"+5017−10750","tz":"America/Swift_Current","region":"CST - SK (midwest)","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"HN","latlong":"+1406−08713","tz":"America/Tegucigalpa","region":"","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"GL","latlong":"+7634−06847","tz":"America/Thule","region":"Thule/Pituffik","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"CA","latlong":"+4823−08915","tz":"America/Thunder_Bay","region":"Eastern - ON (Thunder Bay)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"MX","latlong":"+3232−11701","tz":"America/Tijuana","region":"Pacific Time US - Baja California","status":"Canonical","offset":"−08:00","dst_offset":"−07:00","notes":""},{"country":"CA","latlong":"+4339−07923","tz":"America/Toronto","region":"Eastern - ON, QC (most areas)","status":"Canonical","offset":"−05:00","dst_offset":"−04:00","notes":""},{"country":"VG","latlong":"+1827−06437","tz":"America/Tortola","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"CA","latlong":"+4916−12307","tz":"America/Vancouver","region":"Pacific - BC (most areas)","status":"Canonical","offset":"−08:00","dst_offset":"−07:00","notes":""},{"country":"","latlong":"","tz":"America/Virgin","region":"","status":"Alias","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Port_of_Spain"},{"country":"CA","latlong":"+6043−13503","tz":"America/Whitehorse","region":"Pacific - Yukon (south)","status":"Canonical","offset":"−08:00","dst_offset":"−07:00","notes":""},{"country":"CA","latlong":"+4953−09709","tz":"America/Winnipeg","region":"Central - ON (west); Manitoba","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"US","latlong":"+593249−1394338","tz":"America/Yakutat","region":"Alaska - Yakutat","status":"Canonical","offset":"−09:00","dst_offset":"−08:00","notes":""},{"country":"CA","latlong":"+6227−11421","tz":"America/Yellowknife","region":"Mountain - NT (central)","status":"Canonical","offset":"−07:00","dst_offset":"−06:00","notes":""},{"country":"AQ","latlong":"−6617+11031","tz":"Antarctica/Casey","region":"Casey","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"AQ","latlong":"−6835+07758","tz":"Antarctica/Davis","region":"Davis","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"AQ","latlong":"−6640+14001","tz":"Antarctica/DumontDUrville","region":"Dumont-d'Urville","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"AU","latlong":"−5430+15857","tz":"Antarctica/Macquarie","region":"Macquarie Island","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"AQ","latlong":"−6736+06253","tz":"Antarctica/Mawson","region":"Mawson","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"AQ","latlong":"−7750+16636","tz":"Antarctica/McMurdo","region":"New Zealand time - McMurdo, South Pole","status":"Alias","offset":"+12:00","dst_offset":"+13:00","notes":"Link to Pacific/Auckland"},{"country":"AQ","latlong":"−6448−06406","tz":"Antarctica/Palmer","region":"Palmer","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":"Chilean Antarctica Region"},{"country":"AQ","latlong":"−6734−06808","tz":"Antarctica/Rothera","region":"Rothera","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"","latlong":"","tz":"Antarctica/South_Pole","region":"","status":"Alias","offset":"+12:00","dst_offset":"+13:00","notes":"Link to Pacific/Auckland"},{"country":"AQ","latlong":"−690022+0393524","tz":"Antarctica/Syowa","region":"Syowa","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"AQ","latlong":"−720041+0023206","tz":"Antarctica/Troll","region":"Troll","status":"Canonical","offset":"+00:00","dst_offset":"+02:00","notes":"Previously used +01:00 for a brief period between standard and daylight time.[2]"},{"country":"AQ","latlong":"−7824+10654","tz":"Antarctica/Vostok","region":"Vostok","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":""},{"country":"SJ","latlong":"+7800+01600","tz":"Arctic/Longyearbyen","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Oslo"},{"country":"YE","latlong":"+1245+04512","tz":"Asia/Aden","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Asia/Riyadh"},{"country":"KZ","latlong":"+4315+07657","tz":"Asia/Almaty","region":"Kazakhstan (most areas)","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":""},{"country":"JO","latlong":"+3157+03556","tz":"Asia/Amman","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"RU","latlong":"+6445+17729","tz":"Asia/Anadyr","region":"MSK+09 - Bering Sea","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"KZ","latlong":"+4431+05016","tz":"Asia/Aqtau","region":"Mangghystau/Mankistau","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"KZ","latlong":"+5017+05710","tz":"Asia/Aqtobe","region":"Aqtobe/Aktobe","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"TM","latlong":"+3757+05823","tz":"Asia/Ashgabat","region":"","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"","latlong":"","tz":"Asia/Ashkhabad","region":"","status":"Alias","offset":"+05:00","dst_offset":"+05:00","notes":"Link to Asia/Ashgabat"},{"country":"KZ","latlong":"+4707+05156","tz":"Asia/Atyrau","region":"Atyrau/Atirau/Gur'yev","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"IQ","latlong":"+3321+04425","tz":"Asia/Baghdad","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"BH","latlong":"+2623+05035","tz":"Asia/Bahrain","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Asia/Qatar"},{"country":"AZ","latlong":"+4023+04951","tz":"Asia/Baku","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"TH","latlong":"+1345+10031","tz":"Asia/Bangkok","region":"","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"RU","latlong":"+5322+08345","tz":"Asia/Barnaul","region":"MSK+04 - Altai","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"LB","latlong":"+3353+03530","tz":"Asia/Beirut","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"KG","latlong":"+4254+07436","tz":"Asia/Bishkek","region":"","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":""},{"country":"BN","latlong":"+0456+11455","tz":"Asia/Brunei","region":"","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"","latlong":"","tz":"Asia/Calcutta","region":"","status":"Alias","offset":"+05:30","dst_offset":"+05:30","notes":"Link to Asia/Kolkata"},{"country":"RU","latlong":"+5203+11328","tz":"Asia/Chita","region":"MSK+06 - Zabaykalsky","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"MN","latlong":"+4804+11430","tz":"Asia/Choibalsan","region":"Dornod, Sukhbaatar","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"","latlong":"","tz":"Asia/Chongqing","region":"","status":"Alias","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Shanghai"},{"country":"","latlong":"","tz":"Asia/Chungking","region":"","status":"Alias","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Shanghai"},{"country":"LK","latlong":"+0656+07951","tz":"Asia/Colombo","region":"","status":"Canonical","offset":"+05:30","dst_offset":"+05:30","notes":""},{"country":"","latlong":"","tz":"Asia/Dacca","region":"","status":"Alias","offset":"+06:00","dst_offset":"+06:00","notes":"Link to Asia/Dhaka"},{"country":"SY","latlong":"+3330+03618","tz":"Asia/Damascus","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"BD","latlong":"+2343+09025","tz":"Asia/Dhaka","region":"","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":""},{"country":"TL","latlong":"−0833+12535","tz":"Asia/Dili","region":"","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"AE","latlong":"+2518+05518","tz":"Asia/Dubai","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"TJ","latlong":"+3835+06848","tz":"Asia/Dushanbe","region":"","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"CY","latlong":"+3507+03357","tz":"Asia/Famagusta","region":"Northern Cyprus","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"PS","latlong":"+3130+03428","tz":"Asia/Gaza","region":"Gaza Strip","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"","latlong":"","tz":"Asia/Harbin","region":"","status":"Alias","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Shanghai"},{"country":"PS","latlong":"+3132+0350542","tz":"Asia/Hebron","region":"West Bank","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"VN","latlong":"+1045+10640","tz":"Asia/Ho_Chi_Minh","region":"","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"HK","latlong":"+2217+11409","tz":"Asia/Hong_Kong","region":"","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"MN","latlong":"+4801+09139","tz":"Asia/Hovd","region":"Bayan-Olgiy, Govi-Altai, Hovd, Uvs, Zavkhan","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"RU","latlong":"+5216+10420","tz":"Asia/Irkutsk","region":"MSK+05 - Irkutsk, Buryatia","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"","latlong":"","tz":"Asia/Istanbul","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Europe/Istanbul"},{"country":"ID","latlong":"−0610+10648","tz":"Asia/Jakarta","region":"Java, Sumatra","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"ID","latlong":"−0232+14042","tz":"Asia/Jayapura","region":"New Guinea (West Papua / Irian Jaya); Malukus/Moluccas","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"IL","latlong":"+314650+0351326","tz":"Asia/Jerusalem","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"AF","latlong":"+3431+06912","tz":"Asia/Kabul","region":"","status":"Canonical","offset":"+04:30","dst_offset":"+04:30","notes":""},{"country":"RU","latlong":"+5301+15839","tz":"Asia/Kamchatka","region":"MSK+09 - Kamchatka","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"PK","latlong":"+2452+06703","tz":"Asia/Karachi","region":"","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"","latlong":"","tz":"Asia/Kashgar","region":"","status":"Alias","offset":"+06:00","dst_offset":"+06:00","notes":"Link to Asia/Urumqi[note 1]"},{"country":"NP","latlong":"+2743+08519","tz":"Asia/Kathmandu","region":"","status":"Canonical","offset":"+05:45","dst_offset":"+05:45","notes":""},{"country":"","latlong":"","tz":"Asia/Katmandu","region":"","status":"Alias","offset":"+05:45","dst_offset":"+05:45","notes":"Link to Asia/Kathmandu"},{"country":"RU","latlong":"+623923+1353314","tz":"Asia/Khandyga","region":"MSK+06 - Tomponsky, Ust-Maysky","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"IN","latlong":"+2232+08822","tz":"Asia/Kolkata","region":"","status":"Canonical","offset":"+05:30","dst_offset":"+05:30","notes":"Note: Different zones in history, see Time in India."},{"country":"RU","latlong":"+5601+09250","tz":"Asia/Krasnoyarsk","region":"MSK+04 - Krasnoyarsk area","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"MY","latlong":"+0310+10142","tz":"Asia/Kuala_Lumpur","region":"Malaysia (peninsula)","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"MY","latlong":"+0133+11020","tz":"Asia/Kuching","region":"Sabah, Sarawak","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"KW","latlong":"+2920+04759","tz":"Asia/Kuwait","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Asia/Riyadh"},{"country":"","latlong":"","tz":"Asia/Macao","region":"","status":"Alias","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Macau"},{"country":"MO","latlong":"+2214+11335","tz":"Asia/Macau","region":"","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"RU","latlong":"+5934+15048","tz":"Asia/Magadan","region":"MSK+08 - Magadan","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"ID","latlong":"−0507+11924","tz":"Asia/Makassar","region":"Borneo (east, south); Sulawesi/Celebes, Bali, Nusa Tengarra; Timor (west)","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"PH","latlong":"+1435+12100","tz":"Asia/Manila","region":"","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"OM","latlong":"+2336+05835","tz":"Asia/Muscat","region":"","status":"Alias","offset":"+04:00","dst_offset":"+04:00","notes":"Link to Asia/Dubai"},{"country":"RU","latlong":"+5345+08707","tz":"Asia/Novokuznetsk","region":"MSK+04 - Kemerovo","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"RU","latlong":"+5502+08255","tz":"Asia/Novosibirsk","region":"MSK+04 - Novosibirsk","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"RU","latlong":"+5500+07324","tz":"Asia/Omsk","region":"MSK+03 - Omsk","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":""},{"country":"KZ","latlong":"+5113+05121","tz":"Asia/Oral","region":"West Kazakhstan","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"KH","latlong":"+1133+10455","tz":"Asia/Phnom_Penh","region":"","status":"Alias","offset":"+07:00","dst_offset":"+07:00","notes":"Link to Asia/Bangkok"},{"country":"ID","latlong":"−0002+10920","tz":"Asia/Pontianak","region":"Borneo (west, central)","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"KP","latlong":"+3901+12545","tz":"Asia/Pyongyang","region":"","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"QA","latlong":"+2517+05132","tz":"Asia/Qatar","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"KZ","latlong":"+4448+06528","tz":"Asia/Qyzylorda","region":"Qyzylorda/Kyzylorda/Kzyl-Orda","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"MM","latlong":"+1647+09610","tz":"Asia/Rangoon","region":"","status":"Alias","offset":"+06:30","dst_offset":"+06:30","notes":"Link to Asia/Yangon"},{"country":"SA","latlong":"+2438+04643","tz":"Asia/Riyadh","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"","latlong":"","tz":"Asia/Saigon","region":"","status":"Alias","offset":"+07:00","dst_offset":"+07:00","notes":"Link to Asia/Ho_Chi_Minh"},{"country":"RU","latlong":"+4658+14242","tz":"Asia/Sakhalin","region":"MSK+08 - Sakhalin Island","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"UZ","latlong":"+3940+06648","tz":"Asia/Samarkand","region":"Uzbekistan (west)","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"KR","latlong":"+3733+12658","tz":"Asia/Seoul","region":"","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"CN","latlong":"+3114+12128","tz":"Asia/Shanghai","region":"Beijing Time","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"SG","latlong":"+0117+10351","tz":"Asia/Singapore","region":"","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"RU","latlong":"+6728+15343","tz":"Asia/Srednekolymsk","region":"MSK+08 - Sakha (E); North Kuril Is","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"TW","latlong":"+2503+12130","tz":"Asia/Taipei","region":"","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"UZ","latlong":"+4120+06918","tz":"Asia/Tashkent","region":"Uzbekistan (east)","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"GE","latlong":"+4143+04449","tz":"Asia/Tbilisi","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"IR","latlong":"+3540+05126","tz":"Asia/Tehran","region":"","status":"Canonical","offset":"+03:30","dst_offset":"+04:30","notes":""},{"country":"","latlong":"","tz":"Asia/Tel_Aviv","region":"","status":"Alias","offset":"+02:00","dst_offset":"+03:00","notes":"Link to Asia/Jerusalem"},{"country":"","latlong":"","tz":"Asia/Thimbu","region":"","status":"Alias","offset":"+06:00","dst_offset":"+06:00","notes":"Link to Asia/Thimphu"},{"country":"BT","latlong":"+2728+08939","tz":"Asia/Thimphu","region":"","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":""},{"country":"JP","latlong":"+353916+1394441","tz":"Asia/Tokyo","region":"","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"RU","latlong":"+5630+08458","tz":"Asia/Tomsk","region":"MSK+04 - Tomsk","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"","latlong":"","tz":"Asia/Ujung_Pandang","region":"","status":"Alias","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Makassar"},{"country":"MN","latlong":"+4755+10653","tz":"Asia/Ulaanbaatar","region":"Mongolia (most areas)","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"","latlong":"","tz":"Asia/Ulan_Bator","region":"","status":"Alias","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Ulaanbaatar"},{"country":"CN","latlong":"+4348+08735","tz":"Asia/Urumqi","region":"Xinjiang Time","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":"The Asia/Urumqi entry in the tz database reflected the use of Xinjiang Time by part of the local population. Consider using Asia/Shanghai for Beijing Time if that is preferred."},{"country":"RU","latlong":"+643337+1431336","tz":"Asia/Ust-Nera","region":"MSK+07 - Oymyakonsky","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"LA","latlong":"+1758+10236","tz":"Asia/Vientiane","region":"","status":"Alias","offset":"+07:00","dst_offset":"+07:00","notes":"Link to Asia/Bangkok"},{"country":"RU","latlong":"+4310+13156","tz":"Asia/Vladivostok","region":"MSK+07 - Amur River","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"RU","latlong":"+6200+12940","tz":"Asia/Yakutsk","region":"MSK+06 - Lena River","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"MM","latlong":"+1647+09610","tz":"Asia/Yangon","region":"","status":"Canonical","offset":"+06:30","dst_offset":"+06:30","notes":""},{"country":"RU","latlong":"+5651+06036","tz":"Asia/Yekaterinburg","region":"MSK+02 - Urals","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"AM","latlong":"+4011+04430","tz":"Asia/Yerevan","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"PT","latlong":"+3744−02540","tz":"Atlantic/Azores","region":"Azores","status":"Canonical","offset":"−01:00","dst_offset":"+00:00","notes":""},{"country":"BM","latlong":"+3217−06446","tz":"Atlantic/Bermuda","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−03:00","notes":""},{"country":"ES","latlong":"+2806−01524","tz":"Atlantic/Canary","region":"Canary Islands","status":"Canonical","offset":"+00:00","dst_offset":"+01:00","notes":""},{"country":"CV","latlong":"+1455−02331","tz":"Atlantic/Cape_Verde","region":"","status":"Canonical","offset":"−01:00","dst_offset":"−01:00","notes":""},{"country":"","latlong":"","tz":"Atlantic/Faeroe","region":"","status":"Alias","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Atlantic/Faroe"},{"country":"FO","latlong":"+6201−00646","tz":"Atlantic/Faroe","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+01:00","notes":""},{"country":"","latlong":"","tz":"Atlantic/Jan_Mayen","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Oslo"},{"country":"PT","latlong":"+3238−01654","tz":"Atlantic/Madeira","region":"Madeira Islands","status":"Canonical","offset":"+00:00","dst_offset":"+01:00","notes":""},{"country":"IS","latlong":"+6409−02151","tz":"Atlantic/Reykjavik","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"GS","latlong":"−5416−03632","tz":"Atlantic/South_Georgia","region":"","status":"Canonical","offset":"−02:00","dst_offset":"−02:00","notes":""},{"country":"SH","latlong":"−1555−00542","tz":"Atlantic/St_Helena","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Africa/Abidjan"},{"country":"FK","latlong":"−5142−05751","tz":"Atlantic/Stanley","region":"","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":""},{"country":"","latlong":"","tz":"Australia/ACT","region":"","status":"Deprecated","offset":"+10:00","dst_offset":"+11:00","notes":"Link to Australia/Sydney"},{"country":"AU","latlong":"−3455+13835","tz":"Australia/Adelaide","region":"South Australia","status":"Canonical","offset":"+09:30","dst_offset":"+10:30","notes":""},{"country":"AU","latlong":"−2728+15302","tz":"Australia/Brisbane","region":"Queensland (most areas)","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"AU","latlong":"−3157+14127","tz":"Australia/Broken_Hill","region":"New South Wales (Yancowinna)","status":"Canonical","offset":"+09:30","dst_offset":"+10:30","notes":""},{"country":"","latlong":"","tz":"Australia/Canberra","region":"","status":"Alias","offset":"+10:00","dst_offset":"+11:00","notes":"Link to Australia/Sydney"},{"country":"AU","latlong":"−3956+14352","tz":"Australia/Currie","region":"Tasmania (King Island)","status":"Canonical","offset":"+10:00","dst_offset":"+11:00","notes":""},{"country":"AU","latlong":"−1228+13050","tz":"Australia/Darwin","region":"Northern Territory","status":"Canonical","offset":"+09:30","dst_offset":"+09:30","notes":""},{"country":"AU","latlong":"−3143+12852","tz":"Australia/Eucla","region":"Western Australia (Eucla)","status":"Canonical","offset":"+08:45","dst_offset":"+08:45","notes":""},{"country":"AU","latlong":"−4253+14719","tz":"Australia/Hobart","region":"Tasmania (most areas)","status":"Canonical","offset":"+10:00","dst_offset":"+11:00","notes":""},{"country":"","latlong":"","tz":"Australia/LHI","region":"","status":"Deprecated","offset":"+10:30","dst_offset":"+11:00","notes":"Link to Australia/Lord_Howe"},{"country":"AU","latlong":"−2016+14900","tz":"Australia/Lindeman","region":"Queensland (Whitsunday Islands)","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"AU","latlong":"−3133+15905","tz":"Australia/Lord_Howe","region":"Lord Howe Island","status":"Canonical","offset":"+10:30","dst_offset":"+11:00","notes":"This is the only time zone in the world that uses 30-minute DST transitions."},{"country":"AU","latlong":"−3749+14458","tz":"Australia/Melbourne","region":"Victoria","status":"Canonical","offset":"+10:00","dst_offset":"+11:00","notes":""},{"country":"","latlong":"","tz":"Australia/North","region":"","status":"Deprecated","offset":"+09:30","dst_offset":"+09:30","notes":"Link to Australia/Darwin"},{"country":"","latlong":"","tz":"Australia/NSW","region":"","status":"Deprecated","offset":"+10:00","dst_offset":"+11:00","notes":"Link to Australia/Sydney"},{"country":"AU","latlong":"−3157+11551","tz":"Australia/Perth","region":"Western Australia (most areas)","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":""},{"country":"","latlong":"","tz":"Australia/Queensland","region":"","status":"Deprecated","offset":"+10:00","dst_offset":"+10:00","notes":"Link to Australia/Brisbane"},{"country":"","latlong":"","tz":"Australia/South","region":"","status":"Deprecated","offset":"+09:30","dst_offset":"+10:30","notes":"Link to Australia/Adelaide"},{"country":"AU","latlong":"−3352+15113","tz":"Australia/Sydney","region":"New South Wales (most areas)","status":"Canonical","offset":"+10:00","dst_offset":"+11:00","notes":""},{"country":"","latlong":"","tz":"Australia/Tasmania","region":"","status":"Deprecated","offset":"+10:00","dst_offset":"+11:00","notes":"Link to Australia/Hobart"},{"country":"","latlong":"","tz":"Australia/Victoria","region":"","status":"Deprecated","offset":"+10:00","dst_offset":"+11:00","notes":"Link to Australia/Melbourne"},{"country":"","latlong":"","tz":"Australia/West","region":"","status":"Deprecated","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Australia/Perth"},{"country":"","latlong":"","tz":"Australia/Yancowinna","region":"","status":"Alias","offset":"+09:30","dst_offset":"+10:30","notes":"Link to Australia/Broken_Hill"},{"country":"","latlong":"","tz":"Brazil/Acre","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−05:00","notes":"Link to America/Rio_Branco"},{"country":"","latlong":"","tz":"Brazil/DeNoronha","region":"","status":"Deprecated","offset":"−02:00","dst_offset":"−02:00","notes":"Link to America/Noronha"},{"country":"","latlong":"","tz":"Brazil/East","region":"","status":"Deprecated","offset":"−03:00","dst_offset":"−02:00","notes":"Link to America/Sao_Paulo"},{"country":"","latlong":"","tz":"Brazil/West","region":"","status":"Deprecated","offset":"−04:00","dst_offset":"−04:00","notes":"Link to America/Manaus"},{"country":"","latlong":"","tz":"Canada/Atlantic","region":"","status":"Deprecated","offset":"−04:00","dst_offset":"−03:00","notes":"Link to America/Halifax"},{"country":"","latlong":"","tz":"Canada/Central","region":"","status":"Deprecated","offset":"−06:00","dst_offset":"−05:00","notes":"Link to America/Winnipeg"},{"country":"","latlong":"","tz":"Canada/Eastern","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Toronto"},{"country":"","latlong":"","tz":"Canada/Mountain","region":"","status":"Deprecated","offset":"−07:00","dst_offset":"−06:00","notes":"Link to America/Edmonton"},{"country":"","latlong":"","tz":"Canada/Newfoundland","region":"","status":"Deprecated","offset":"−03:30","dst_offset":"−02:30","notes":"Link to America/St_Johns"},{"country":"","latlong":"","tz":"Canada/Pacific","region":"","status":"Deprecated","offset":"−08:00","dst_offset":"−07:00","notes":"Link to America/Vancouver"},{"country":"","latlong":"","tz":"Canada/Saskatchewan","region":"","status":"Deprecated","offset":"−06:00","dst_offset":"−06:00","notes":"Link to America/Regina"},{"country":"","latlong":"","tz":"Canada/Yukon","region":"","status":"Deprecated","offset":"−08:00","dst_offset":"−07:00","notes":"Link to America/Whitehorse"},{"country":"","latlong":"","tz":"CET","region":"","status":"Deprecated","offset":"+01:00","dst_offset":"+02:00","notes":"Choose a zone that observes CET, such as Europe/Paris."},{"country":"","latlong":"","tz":"Chile/Continental","region":"","status":"Deprecated","offset":"−04:00","dst_offset":"−03:00","notes":"Link to America/Santiago"},{"country":"","latlong":"","tz":"Chile/EasterIsland","region":"","status":"Deprecated","offset":"−06:00","dst_offset":"−05:00","notes":"Link to Pacific/Easter"},{"country":"","latlong":"","tz":"CST6CDT","region":"","status":"Deprecated","offset":"−06:00","dst_offset":"−05:00","notes":"Choose a zone that observes CST with United States daylight saving time rules, such as America/Chicago."},{"country":"","latlong":"","tz":"Cuba","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Havana"},{"country":"","latlong":"","tz":"EET","region":"","status":"Deprecated","offset":"+02:00","dst_offset":"+03:00","notes":"Choose a zone that observes EET, such as Europe/Sofia."},{"country":"","latlong":"","tz":"Egypt","region":"","status":"Deprecated","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Cairo"},{"country":"","latlong":"","tz":"Eire","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/Dublin"},{"country":"","latlong":"","tz":"EST","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−05:00","notes":"Choose a zone that currently observes EST without daylight saving time, such as America/Cancun."},{"country":"","latlong":"","tz":"EST5EDT","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−04:00","notes":"Choose a zone that observes EST with United States daylight saving time rules, such as America/New_York."},{"country":"","latlong":"","tz":"Etc/GMT","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"","latlong":"","tz":"Etc/GMT+0","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"Etc/GMT+1","region":"","status":"Canonical","offset":"−01:00","dst_offset":"−01:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+10","region":"","status":"Canonical","offset":"−10:00","dst_offset":"−10:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+11","region":"","status":"Canonical","offset":"−11:00","dst_offset":"−11:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+12","region":"","status":"Canonical","offset":"−12:00","dst_offset":"−12:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+2","region":"","status":"Canonical","offset":"−02:00","dst_offset":"−02:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+3","region":"","status":"Canonical","offset":"−03:00","dst_offset":"−03:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+4","region":"","status":"Canonical","offset":"−04:00","dst_offset":"−04:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+5","region":"","status":"Canonical","offset":"−05:00","dst_offset":"−05:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+6","region":"","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+7","region":"","status":"Canonical","offset":"−07:00","dst_offset":"−07:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+8","region":"","status":"Canonical","offset":"−08:00","dst_offset":"−08:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT+9","region":"","status":"Canonical","offset":"−09:00","dst_offset":"−09:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT0","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"Etc/GMT-0","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"Etc/GMT-1","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+01:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-10","region":"","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-11","region":"","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-12","region":"","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-13","region":"","status":"Canonical","offset":"+13:00","dst_offset":"+13:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-14","region":"","status":"Canonical","offset":"+14:00","dst_offset":"+14:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-2","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-3","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-4","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-5","region":"","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-6","region":"","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-7","region":"","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-8","region":"","status":"Canonical","offset":"+08:00","dst_offset":"+08:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/GMT-9","region":"","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":"Sign is intentionally inverted. See the Etc area description."},{"country":"","latlong":"","tz":"Etc/Greenwich","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"Etc/UCT","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"","latlong":"","tz":"Etc/Universal","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/UTC"},{"country":"","latlong":"","tz":"Etc/UTC","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+00:00","notes":""},{"country":"","latlong":"","tz":"Etc/Zulu","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/UTC"},{"country":"NL","latlong":"+5222+00454","tz":"Europe/Amsterdam","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"AD","latlong":"+4230+00131","tz":"Europe/Andorra","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"RU","latlong":"+4621+04803","tz":"Europe/Astrakhan","region":"MSK+01 - Astrakhan","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"GR","latlong":"+3758+02343","tz":"Europe/Athens","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"","latlong":"","tz":"Europe/Belfast","region":"","status":"Alias","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/London"},{"country":"RS","latlong":"+4450+02030","tz":"Europe/Belgrade","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"RS","latlong":"+4450+02030","tz":"Europe/Sarajevo","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"DE","latlong":"+5230+01322","tz":"Europe/Berlin","region":"Germany (except for Büsingen am Hochrhein)","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":"In 1945, the Trizone did not follow Berlin's switch to DST, see Time in Germany"},{"country":"SK","latlong":"+4809+01707","tz":"Europe/Bratislava","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Prague"},{"country":"BE","latlong":"+5050+00420","tz":"Europe/Brussels","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"RO","latlong":"+4426+02606","tz":"Europe/Bucharest","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"HU","latlong":"+4730+01905","tz":"Europe/Budapest","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"DE","latlong":"+4742+00841","tz":"Europe/Busingen","region":"Büsingen am Hochrhein","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Zurich"},{"country":"MD","latlong":"+4700+02850","tz":"Europe/Chisinau","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"DK","latlong":"+5540+01235","tz":"Europe/Copenhagen","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"IE","latlong":"+5320−00615","tz":"Europe/Dublin","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+01:00","notes":""},{"country":"GI","latlong":"+3608−00521","tz":"Europe/Gibraltar","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"GG","latlong":"+492717−0023210","tz":"Europe/Guernsey","region":"","status":"Alias","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/London"},{"country":"FI","latlong":"+6010+02458","tz":"Europe/Helsinki","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"IM","latlong":"+5409−00428","tz":"Europe/Isle_of_Man","region":"","status":"Alias","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/London"},{"country":"TR","latlong":"+4101+02858","tz":"Europe/Istanbul","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"JE","latlong":"+491101−0020624","tz":"Europe/Jersey","region":"","status":"Alias","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/London"},{"country":"RU","latlong":"+5443+02030","tz":"Europe/Kaliningrad","region":"MSK−01 - Kaliningrad","status":"Canonical","offset":"+02:00","dst_offset":"+02:00","notes":""},{"country":"UA","latlong":"+5026+03031","tz":"Europe/Kiev","region":"Ukraine (most areas)","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"RU","latlong":"+5836+04939","tz":"Europe/Kirov","region":"MSK+00 - Kirov","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"PT","latlong":"+3843−00908","tz":"Europe/Lisbon","region":"Portugal (mainland)","status":"Canonical","offset":"+00:00","dst_offset":"+01:00","notes":""},{"country":"SI","latlong":"+4603+01431","tz":"Europe/Ljubljana","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Belgrade"},{"country":"GB","latlong":"+513030−0000731","tz":"Europe/London","region":"","status":"Canonical","offset":"+00:00","dst_offset":"+01:00","notes":""},{"country":"LU","latlong":"+4936+00609","tz":"Europe/Luxembourg","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"ES","latlong":"+4024−00341","tz":"Europe/Madrid","region":"Spain (mainland)","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"MT","latlong":"+3554+01431","tz":"Europe/Malta","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"AX","latlong":"+6006+01957","tz":"Europe/Mariehamn","region":"","status":"Alias","offset":"+02:00","dst_offset":"+03:00","notes":"Link to Europe/Helsinki"},{"country":"BY","latlong":"+5354+02734","tz":"Europe/Minsk","region":"","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"MC","latlong":"+4342+00723","tz":"Europe/Monaco","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"RU","latlong":"+554521+0373704","tz":"Europe/Moscow","region":"MSK+00 - Moscow area","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":""},{"country":"CY","latlong":"+3510+03322","tz":"Asia/Nicosia","region":"Cyprus (most areas)","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"NO","latlong":"+5955+01045","tz":"Europe/Oslo","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"FR","latlong":"+4852+00220","tz":"Europe/Paris","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"ME","latlong":"+4226+01916","tz":"Europe/Podgorica","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Belgrade"},{"country":"CZ","latlong":"+5005+01426","tz":"Europe/Prague","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"LV","latlong":"+5657+02406","tz":"Europe/Riga","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"IT","latlong":"+4154+01229","tz":"Europe/Rome","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"RU","latlong":"+5312+05009","tz":"Europe/Samara","region":"MSK+01 - Samara, Udmurtia","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"SM","latlong":"+4355+01228","tz":"Europe/San_Marino","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Rome"},{"country":"BA","latlong":"+4352+01825","tz":"Europe/Sarajevo","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Belgrade"},{"country":"RU","latlong":"+5134+04602","tz":"Europe/Saratov","region":"MSK+01 - Saratov","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"UA","latlong":"+4457+03406","tz":"Europe/Simferopol","region":"Crimea","status":"Canonical","offset":"+03:00","dst_offset":"+03:00","notes":"Disputed - Reflects data in the TZDB.[note 2]"},{"country":"MK","latlong":"+4159+02126","tz":"Europe/Skopje","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Belgrade"},{"country":"BG","latlong":"+4241+02319","tz":"Europe/Sofia","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"SE","latlong":"+5920+01803","tz":"Europe/Stockholm","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"EE","latlong":"+5925+02445","tz":"Europe/Tallinn","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"AL","latlong":"+4120+01950","tz":"Europe/Tirane","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"","latlong":"","tz":"Europe/Tiraspol","region":"","status":"Alias","offset":"+02:00","dst_offset":"+03:00","notes":"Link to Europe/Chisinau"},{"country":"RU","latlong":"+5420+04824","tz":"Europe/Ulyanovsk","region":"MSK+01 - Ulyanovsk","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"UA","latlong":"+4837+02218","tz":"Europe/Uzhgorod","region":"Ruthenia","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"LI","latlong":"+4709+00931","tz":"Europe/Vaduz","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Zurich"},{"country":"VA","latlong":"+415408+0122711","tz":"Europe/Vatican","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Rome"},{"country":"AT","latlong":"+4813+01620","tz":"Europe/Vienna","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"LT","latlong":"+5441+02519","tz":"Europe/Vilnius","region":"","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"RU","latlong":"+4844+04425","tz":"Europe/Volgograd","region":"MSK+01 - Volgograd","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"PL","latlong":"+5215+02100","tz":"Europe/Warsaw","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"HR","latlong":"+4548+01558","tz":"Europe/Zagreb","region":"","status":"Alias","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Belgrade"},{"country":"UA","latlong":"+4750+03510","tz":"Europe/Zaporozhye","region":"Zaporozh'ye/Zaporizhia; Lugansk/Luhansk (east)","status":"Canonical","offset":"+02:00","dst_offset":"+03:00","notes":""},{"country":"CH","latlong":"+4723+00832","tz":"Europe/Zurich","region":"","status":"Canonical","offset":"+01:00","dst_offset":"+02:00","notes":""},{"country":"","latlong":"","tz":"GB","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/London"},{"country":"","latlong":"","tz":"GB-Eire","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/London"},{"country":"","latlong":"","tz":"GMT","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"GMT+0","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"GMT0","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"GMT-0","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"Greenwich","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/GMT"},{"country":"","latlong":"","tz":"Hongkong","region":"","status":"Deprecated","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Hong_Kong"},{"country":"","latlong":"","tz":"HST","region":"","status":"Deprecated","offset":"−10:00","dst_offset":"−10:00","notes":"Choose a zone that currently observes HST without daylight saving time, such as Pacific/Honolulu."},{"country":"","latlong":"","tz":"Iceland","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Atlantic/Reykjavik"},{"country":"MG","latlong":"−1855+04731","tz":"Indian/Antananarivo","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"IO","latlong":"−0720+07225","tz":"Indian/Chagos","region":"","status":"Canonical","offset":"+06:00","dst_offset":"+06:00","notes":""},{"country":"CX","latlong":"−1025+10543","tz":"Indian/Christmas","region":"","status":"Canonical","offset":"+07:00","dst_offset":"+07:00","notes":""},{"country":"CC","latlong":"−1210+09655","tz":"Indian/Cocos","region":"","status":"Canonical","offset":"+06:30","dst_offset":"+06:30","notes":""},{"country":"KM","latlong":"−1141+04316","tz":"Indian/Comoro","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"TF","latlong":"−492110+0701303","tz":"Indian/Kerguelen","region":"","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"SC","latlong":"−0440+05528","tz":"Indian/Mahe","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"MV","latlong":"+0410+07330","tz":"Indian/Maldives","region":"","status":"Canonical","offset":"+05:00","dst_offset":"+05:00","notes":""},{"country":"MU","latlong":"−2010+05730","tz":"Indian/Mauritius","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"YT","latlong":"−1247+04514","tz":"Indian/Mayotte","region":"","status":"Alias","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Africa/Nairobi"},{"country":"RE","latlong":"−2052+05528","tz":"Indian/Reunion","region":"","status":"Canonical","offset":"+04:00","dst_offset":"+04:00","notes":""},{"country":"","latlong":"","tz":"Iran","region":"","status":"Deprecated","offset":"+03:30","dst_offset":"+04:30","notes":"Link to Asia/Tehran"},{"country":"","latlong":"","tz":"Israel","region":"","status":"Deprecated","offset":"+02:00","dst_offset":"+03:00","notes":"Link to Asia/Jerusalem"},{"country":"","latlong":"","tz":"Jamaica","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−05:00","notes":"Link to America/Jamaica"},{"country":"","latlong":"","tz":"Japan","region":"","status":"Deprecated","offset":"+09:00","dst_offset":"+09:00","notes":"Link to Asia/Tokyo"},{"country":"","latlong":"","tz":"Kwajalein","region":"","status":"Deprecated","offset":"+12:00","dst_offset":"+12:00","notes":"Link to Pacific/Kwajalein"},{"country":"","latlong":"","tz":"Libya","region":"","status":"Deprecated","offset":"+02:00","dst_offset":"+02:00","notes":"Link to Africa/Tripoli"},{"country":"","latlong":"","tz":"MET","region":"","status":"Deprecated","offset":"+01:00","dst_offset":"+02:00","notes":"Choose a zone that observes MET (sames as CET), such as Europe/Paris."},{"country":"","latlong":"","tz":"Mexico/BajaNorte","region":"","status":"Deprecated","offset":"−08:00","dst_offset":"−07:00","notes":"Link to America/Tijuana"},{"country":"","latlong":"","tz":"Mexico/BajaSur","region":"","status":"Deprecated","offset":"−07:00","dst_offset":"−06:00","notes":"Link to America/Mazatlan"},{"country":"","latlong":"","tz":"Mexico/General","region":"","status":"Deprecated","offset":"−06:00","dst_offset":"−05:00","notes":"Link to America/Mexico_City"},{"country":"","latlong":"","tz":"MST","region":"","status":"Deprecated","offset":"−07:00","dst_offset":"−07:00","notes":"Choose a zone that currently observes MST without daylight saving time, such as America/Phoenix."},{"country":"","latlong":"","tz":"MST7MDT","region":"","status":"Deprecated","offset":"−07:00","dst_offset":"−06:00","notes":"Choose a zone that observes MST with United States daylight saving time rules, such as America/Denver."},{"country":"","latlong":"","tz":"Navajo","region":"","status":"Deprecated","offset":"−07:00","dst_offset":"−06:00","notes":"Link to America/Denver"},{"country":"","latlong":"","tz":"NZ","region":"","status":"Deprecated","offset":"+12:00","dst_offset":"+13:00","notes":"Link to Pacific/Auckland"},{"country":"","latlong":"","tz":"NZ-CHAT","region":"","status":"Deprecated","offset":"+12:45","dst_offset":"+13:45","notes":"Link to Pacific/Chatham"},{"country":"WS","latlong":"−1350−17144","tz":"Pacific/Apia","region":"","status":"Canonical","offset":"+13:00","dst_offset":"+14:00","notes":""},{"country":"NZ","latlong":"−3652+17446","tz":"Pacific/Auckland","region":"New Zealand (most areas)","status":"Canonical","offset":"+12:00","dst_offset":"+13:00","notes":""},{"country":"PG","latlong":"−0613+15534","tz":"Pacific/Bougainville","region":"Bougainville","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"NZ","latlong":"−4357−17633","tz":"Pacific/Chatham","region":"Chatham Islands","status":"Canonical","offset":"+12:45","dst_offset":"+13:45","notes":""},{"country":"FM","latlong":"+0725+15147","tz":"Pacific/Chuuk","region":"Chuuk/Truk, Yap","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"CL","latlong":"−2709−10926","tz":"Pacific/Easter","region":"Easter Island","status":"Canonical","offset":"−06:00","dst_offset":"−05:00","notes":""},{"country":"VU","latlong":"−1740+16825","tz":"Pacific/Efate","region":"","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"KI","latlong":"−0308−17105","tz":"Pacific/Enderbury","region":"Phoenix Islands","status":"Canonical","offset":"+13:00","dst_offset":"+13:00","notes":""},{"country":"TK","latlong":"−0922−17114","tz":"Pacific/Fakaofo","region":"","status":"Canonical","offset":"+13:00","dst_offset":"+13:00","notes":""},{"country":"FJ","latlong":"−1808+17825","tz":"Pacific/Fiji","region":"","status":"Canonical","offset":"+12:00","dst_offset":"+13:00","notes":""},{"country":"TV","latlong":"−0831+17913","tz":"Pacific/Funafuti","region":"","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"EC","latlong":"−0054−08936","tz":"Pacific/Galapagos","region":"Galapagos Islands","status":"Canonical","offset":"−06:00","dst_offset":"−06:00","notes":""},{"country":"PF","latlong":"−2308−13457","tz":"Pacific/Gambier","region":"Gambier Islands","status":"Canonical","offset":"−09:00","dst_offset":"−09:00","notes":""},{"country":"SB","latlong":"−0932+16012","tz":"Pacific/Guadalcanal","region":"","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"GU","latlong":"+1328+14445","tz":"Pacific/Guam","region":"","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"US","latlong":"+211825−1575130","tz":"Pacific/Honolulu","region":"Hawaii","status":"Canonical","offset":"−10:00","dst_offset":"−10:00","notes":""},{"country":"","latlong":"","tz":"Pacific/Johnston","region":"","status":"Alias","offset":"−10:00","dst_offset":"−10:00","notes":"Link to Pacific/Honolulu"},{"country":"KI","latlong":"+0152−15720","tz":"Pacific/Kiritimati","region":"Line Islands","status":"Canonical","offset":"+14:00","dst_offset":"+14:00","notes":""},{"country":"FM","latlong":"+0519+16259","tz":"Pacific/Kosrae","region":"Kosrae","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"MH","latlong":"+0905+16720","tz":"Pacific/Kwajalein","region":"Kwajalein","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"MH","latlong":"+0709+17112","tz":"Pacific/Majuro","region":"Marshall Islands (most areas)","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"PF","latlong":"−0900−13930","tz":"Pacific/Marquesas","region":"Marquesas Islands","status":"Canonical","offset":"−09:30","dst_offset":"−09:30","notes":""},{"country":"UM","latlong":"+2813−17722","tz":"Pacific/Midway","region":"Midway Islands","status":"Alias","offset":"−11:00","dst_offset":"−11:00","notes":"Link to Pacific/Pago_Pago"},{"country":"NR","latlong":"−0031+16655","tz":"Pacific/Nauru","region":"","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"NU","latlong":"−1901−16955","tz":"Pacific/Niue","region":"","status":"Canonical","offset":"−11:00","dst_offset":"−11:00","notes":""},{"country":"NF","latlong":"−2903+16758","tz":"Pacific/Norfolk","region":"","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"NC","latlong":"−2216+16627","tz":"Pacific/Noumea","region":"","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"AS","latlong":"−1416−17042","tz":"Pacific/Pago_Pago","region":"","status":"Canonical","offset":"−11:00","dst_offset":"−11:00","notes":""},{"country":"PW","latlong":"+0720+13429","tz":"Pacific/Palau","region":"","status":"Canonical","offset":"+09:00","dst_offset":"+09:00","notes":""},{"country":"PN","latlong":"−2504−13005","tz":"Pacific/Pitcairn","region":"","status":"Canonical","offset":"−08:00","dst_offset":"−08:00","notes":""},{"country":"FM","latlong":"+0658+15813","tz":"Pacific/Pohnpei","region":"Pohnpei/Ponape","status":"Canonical","offset":"+11:00","dst_offset":"+11:00","notes":""},{"country":"","latlong":"","tz":"Pacific/Ponape","region":"","status":"Alias","offset":"+11:00","dst_offset":"+11:00","notes":"Link to Pacific/Pohnpei"},{"country":"PG","latlong":"−0930+14710","tz":"Pacific/Port_Moresby","region":"Papua New Guinea (most areas)","status":"Canonical","offset":"+10:00","dst_offset":"+10:00","notes":""},{"country":"CK","latlong":"−2114−15946","tz":"Pacific/Rarotonga","region":"","status":"Canonical","offset":"−10:00","dst_offset":"−10:00","notes":""},{"country":"MP","latlong":"+1512+14545","tz":"Pacific/Saipan","region":"","status":"Alias","offset":"+10:00","dst_offset":"+10:00","notes":"Link to Pacific/Guam"},{"country":"","latlong":"","tz":"Pacific/Samoa","region":"","status":"Alias","offset":"−11:00","dst_offset":"−11:00","notes":"Link to Pacific/Pago_Pago"},{"country":"PF","latlong":"−1732−14934","tz":"Pacific/Tahiti","region":"Society Islands","status":"Canonical","offset":"−10:00","dst_offset":"−10:00","notes":""},{"country":"KI","latlong":"+0125+17300","tz":"Pacific/Tarawa","region":"Gilbert Islands","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"TO","latlong":"−2110−17510","tz":"Pacific/Tongatapu","region":"","status":"Canonical","offset":"+13:00","dst_offset":"+14:00","notes":""},{"country":"","latlong":"","tz":"Pacific/Truk","region":"","status":"Alias","offset":"+10:00","dst_offset":"+10:00","notes":"Link to Pacific/Chuuk"},{"country":"UM","latlong":"+1917+16637","tz":"Pacific/Wake","region":"Wake Island","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"WF","latlong":"−1318−17610","tz":"Pacific/Wallis","region":"","status":"Canonical","offset":"+12:00","dst_offset":"+12:00","notes":""},{"country":"","latlong":"","tz":"Pacific/Yap","region":"","status":"Alias","offset":"+10:00","dst_offset":"+10:00","notes":"Link to Pacific/Chuuk"},{"country":"","latlong":"","tz":"Poland","region":"","status":"Deprecated","offset":"+01:00","dst_offset":"+02:00","notes":"Link to Europe/Warsaw"},{"country":"","latlong":"","tz":"Portugal","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+01:00","notes":"Link to Europe/Lisbon"},{"country":"","latlong":"","tz":"PRC","region":"","status":"Deprecated","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Shanghai"},{"country":"","latlong":"","tz":"PST8PDT","region":"","status":"Deprecated","offset":"−08:00","dst_offset":"−07:00","notes":"Choose a zone that observes PST with United States daylight saving time rules, such as America/Los_Angeles."},{"country":"","latlong":"","tz":"ROC","region":"","status":"Deprecated","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Taipei"},{"country":"","latlong":"","tz":"ROK","region":"","status":"Deprecated","offset":"+09:00","dst_offset":"+09:00","notes":"Link to Asia/Seoul"},{"country":"","latlong":"","tz":"Singapore","region":"","status":"Deprecated","offset":"+08:00","dst_offset":"+08:00","notes":"Link to Asia/Singapore"},{"country":"","latlong":"","tz":"Turkey","region":"","status":"Deprecated","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Europe/Istanbul"},{"country":"","latlong":"","tz":"UCT","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/UCT"},{"country":"","latlong":"","tz":"Universal","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/UTC"},{"country":"","latlong":"","tz":"US/Alaska","region":"","status":"Deprecated","offset":"−09:00","dst_offset":"−08:00","notes":"Link to America/Anchorage"},{"country":"","latlong":"","tz":"US/Aleutian","region":"","status":"Deprecated","offset":"−10:00","dst_offset":"−09:00","notes":"Link to America/Adak"},{"country":"","latlong":"","tz":"US/Arizona","region":"","status":"Deprecated","offset":"−07:00","dst_offset":"−07:00","notes":"Link to America/Phoenix"},{"country":"","latlong":"","tz":"US/Central","region":"","status":"Deprecated","offset":"−06:00","dst_offset":"−05:00","notes":"Link to America/Chicago"},{"country":"","latlong":"","tz":"US/Eastern","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/New_York"},{"country":"","latlong":"","tz":"US/East-Indiana","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Indiana/Indianapolis"},{"country":"","latlong":"","tz":"US/Hawaii","region":"","status":"Deprecated","offset":"−10:00","dst_offset":"−10:00","notes":"Link to Pacific/Honolulu"},{"country":"","latlong":"","tz":"US/Indiana-Starke","region":"","status":"Deprecated","offset":"−06:00","dst_offset":"−05:00","notes":"Link to America/Indiana/Knox"},{"country":"","latlong":"","tz":"US/Michigan","region":"","status":"Deprecated","offset":"−05:00","dst_offset":"−04:00","notes":"Link to America/Detroit"},{"country":"","latlong":"","tz":"US/Mountain","region":"","status":"Deprecated","offset":"−07:00","dst_offset":"−06:00","notes":"Link to America/Denver"},{"country":"","latlong":"","tz":"US/Pacific","region":"","status":"Deprecated","offset":"−08:00","dst_offset":"−07:00","notes":"Link to America/Los_Angeles"},{"country":"","latlong":"","tz":"US/Pacific-New","region":"","status":"Deprecated","offset":"−08:00","dst_offset":"−07:00","notes":"Link to America/Los_Angeles"},{"country":"","latlong":"","tz":"US/Samoa","region":"","status":"Deprecated","offset":"−11:00","dst_offset":"−11:00","notes":"Link to Pacific/Pago_Pago"},{"country":"","latlong":"","tz":"UTC","region":"","status":"Alias","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/UTC"},{"country":"","latlong":"","tz":"WET","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+01:00","notes":"Choose a zone that observes WET, such as Europe/Lisbon."},{"country":"","latlong":"","tz":"W-SU","region":"","status":"Deprecated","offset":"+03:00","dst_offset":"+03:00","notes":"Link to Europe/Moscow"},{"country":"","latlong":"","tz":"Zulu","region":"","status":"Deprecated","offset":"+00:00","dst_offset":"+00:00","notes":"Link to Etc/UTC\n"}] diff --git a/frontend/src/environments/environment-definition.ts b/frontend/src/environments/environment-definition.ts new file mode 100644 index 00000000..2fa463e8 --- /dev/null +++ b/frontend/src/environments/environment-definition.ts @@ -0,0 +1,9 @@ +export type EnvironmentDefinition = { + contact_mail: string | null, + production: boolean, + ApiHost: string, + aboutPageRender?: string | null, + SSRApiHost?: string | null, + SSRAboutPageRender?: string | null, + YjsWsSyncServer?: string | null, +}; diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts index 7ecccba1..e493b0bd 100644 --- a/frontend/src/environments/environment.prod.ts +++ b/frontend/src/environments/environment.prod.ts @@ -1,5 +1,10 @@ -export const environment = { +import { EnvironmentDefinition } from "./environment-definition"; + +export const environment : EnvironmentDefinition = { contact_mail: null, production: true, ApiHost: '', - }; + SSRApiHost: null, + aboutPageRender: null, + SSRAboutPageRender: null, +}; diff --git a/frontend/src/environments/environment.programaker-dev.ts b/frontend/src/environments/environment.programaker-dev.ts new file mode 100644 index 00000000..523f0826 --- /dev/null +++ b/frontend/src/environments/environment.programaker-dev.ts @@ -0,0 +1,10 @@ +import { EnvironmentDefinition } from "./environment-definition"; + +export const environment : EnvironmentDefinition = { + contact_mail: 'contact@programaker.com', + production: true, + ApiHost: 'https://programaker.com', + aboutPageRender: 'https://programaker.com/api/v0/programs/by-id/5fad1d5e-9f69-4f7f-93db-cbb6816d8b22/render/', + SSRAboutPageRender: null, + YjsWsSyncServer: 'wss://yjs-sync.programaker.com', + }; diff --git a/frontend/src/environments/environment.programaker.ts b/frontend/src/environments/environment.programaker.ts index 88130010..5415230c 100644 --- a/frontend/src/environments/environment.programaker.ts +++ b/frontend/src/environments/environment.programaker.ts @@ -1,5 +1,11 @@ -export const environment = { +import { EnvironmentDefinition } from "./environment-definition"; + +export const environment : EnvironmentDefinition = { contact_mail: 'contact@programaker.com', production: true, ApiHost: '', - }; + SSRApiHost: 'http://plaza-backend:80', + aboutPageRender: 'https://programaker.com/api/v0/programs/by-id/5fad1d5e-9f69-4f7f-93db-cbb6816d8b22/render/', + SSRAboutPageRender: 'http://plaza-backend:80/api/v0/programs/by-id/5fad1d5e-9f69-4f7f-93db-cbb6816d8b22/render/', + YjsWsSyncServer: 'wss://yjs-sync.programaker.com', +}; diff --git a/frontend/src/environments/environment.tests.ts b/frontend/src/environments/environment.tests.ts new file mode 100644 index 00000000..5aab88ca --- /dev/null +++ b/frontend/src/environments/environment.tests.ts @@ -0,0 +1,13 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. + +import { EnvironmentDefinition } from "./environment-definition"; + +export const environment : EnvironmentDefinition = { + contact_mail: 'configure-this@on-environment.com', + production: false, + ApiHost: 'http://localhost:8888', + SSRAboutPageRender: null, +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 4162b6a6..9e48fc1a 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -3,8 +3,13 @@ // `ng build --env=prod` then `environment.prod.ts` will be used instead. // The list of which env maps to which file can be found in `.angular-cli.json`. -export const environment = { +import { EnvironmentDefinition } from "./environment-definition"; + +export const environment : EnvironmentDefinition = { contact_mail: 'configure-this@on-environment.com', production: false, ApiHost: 'http://localhost:8888', - }; + aboutPageRender: 'https://programaker.com/api/v0/programs/by-id/5fad1d5e-9f69-4f7f-93db-cbb6816d8b22/render/', + SSRAboutPageRender: null, + YjsWsSyncServer: 'ws://localhost:1234', +}; diff --git a/frontend/src/flow_editor.scss b/frontend/src/flow_editor.scss new file mode 100644 index 00000000..02d04a47 --- /dev/null +++ b/frontend/src/flow_editor.scss @@ -0,0 +1,1102 @@ +// Colors +$pulse-color: #f0c000; +$user-pulse-color-1: #fa0; +$user-pulse-color-2: #404; +$string-color: #44dd44; +$integer-color: #4444ff; +$float-color: #44dddd; +$boolean-color: #dd4444; +$any-color: #dd44dd; +$list-color: #845801; +$enum-color: #888; +$unknown-color: #7f7f7f; + +// Basics +#workspace > svg { + width: 100%; + height: 100%; + + // Background pattern taken from https://codepen.io/tennowman/pen/ynrih + background-color: #269; + background-image: + linear-gradient(rgba(255,255,255,.3) 2px, transparent 2px), + linear-gradient(90deg, rgba(255,255,255,.3) 2px, transparent 2px), + linear-gradient(rgba(255,255,255,.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,.1) 1px, transparent 1px); + background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px; + background-position: -2px -2px, -2px -2px, -1px -1px, -1px -1px; +} + +// Define variables in a way that it's accessible to elements in the SVG +defs { + \-\-user-pulse-1: $user-pulse-color-1; + \-\-user-pulse-2: $user-pulse-color-2; +} + +svg.block_renderer.dragging { + cursor: grabbing; +} + +svg.block_renderer.selecting { + cursor: crosshair; +} + +svg.block_renderer rect.selection { + display: none; + + fill: rgba(255,255,0,0.3); + stroke-width: 2px; + stroke: #fff; + stroke-dasharray: 5; +} + +svg.block_renderer.selecting rect.selection { + display: block; +} + +svg.block_renderer.read-only g.flow_node > a, +svg.block_renderer.read-only g.flow_node > g +{ + cursor: not-allowed; +} + +svg.block_renderer g.flow_node > a, +svg.block_renderer g.flow_node > g +{ + pointer-events: all; + cursor: grab; +} + +svg.block_renderer.dragging g.flow_node > a, +svg.block_renderer.dragging g.flow_node > g { + cursor: grabbing; +} + + +svg.block_renderer g.flow_node.selected rect.node_body, svg.block_renderer g.flow_node .selected rect.node_body { + stroke: #A05; + stroke-width: 3px; + stroke-dasharray: 0; +} + +// Generic rect control +rect.hidden { + visibility: hidden; +} + +// Block type names are hidden by default +svg.block_renderer .block_type_annotation { + visibility: hidden; +} + +// Only shown on the toolbox showcases +.showcase svg.block_renderer .block_type_annotation { + visibility: visible; +} + +svg.block_renderer path.building { + pointer-events: none; + stroke-width: 4px; +} + +svg.block_renderer path.connection { + stroke-linejoin: round; +} + +svg.block_renderer path.established { + stroke-width: 5px; +} + +svg.block_renderer path.string_wire { + stroke: $string-color; +} + +svg.block_renderer path.integer_wire { + stroke: $integer-color; +} + +svg.block_renderer path.float_wire { + stroke: $float-color; +} + +svg.block_renderer path.boolean_wire { + stroke: $boolean-color; +} + +svg.block_renderer path.enum_wire { + stroke: $enum-color; +} + +svg.block_renderer path.enum_sequence_wire { + stroke: $enum-color; +} + +svg.block_renderer path.any_wire { + stroke: $any-color; +} + +svg.block_renderer path.list_wire { + stroke: $list-color; +} + +svg.block_renderer path.pulse_wire { + stroke: $pulse-color; + stroke-width: 7px; +} + +svg.block_renderer path.user-pulse_wire { + stroke: url(#user-pulse-wire-pattern) $user-pulse-color-1; + stroke-width: 7px; +} + +svg.block_renderer path.unknown_wire { + stroke: $unknown-color; +} + +svg.block_renderer:not(.drawing):not(.dragging) { + path.established:hover { + stroke-width: 10px; + cursor: crosshair; + + &.string_wire { + stroke: #049d04; + } + + &.integer_wire { + stroke: #0404bf; + } + + &.float_wire { + stroke: #04bfbf; + } + + &.boolean_wire { + stroke: #ad0404; + } + + &.any_wire { + stroke: #9d049d; + } + + &.list_wire { + stroke: #9d049d; + } + + &.pulse_wire { + stroke: #b80; + } + + &.user-pulse_wire { + stroke: url(#user-pulse-wire-pattern) $user-pulse-color-1; + } + + &.unknown_wire { + stroke: #3f3f3f; + } + } + + &.read-only path.established:hover { + cursor: not-allowed + } +} + +svg.block_renderer g.flow_node rect.node_body { + fill: #fff; + stroke-width: 0; +} + +svg.block_renderer g.flow_node rect.body_shadow { + // Separated in a different element to avoid problems + // in case filter problems make it disappear + filter: url("#shadow"); + fill: rgba(0,0,0,0.3); // Shadow color +} + +svg.block_renderer g.flow_node .to-be-removed rect.node_body, +svg.block_renderer g.flow_node.to-be-removed rect.node_body { + fill-opacity: 0.50; + fill: #f88; + color: #fff; +} + +.grid-division { + stroke: #88f; + stroke-width: 1px; + stroke-dasharray: 3; + + &.vert-cut { + stroke: #f88; + } + + &.horiz-cut { + stroke: #8f8; + } +} + +svg.block_renderer.drawing rect.node_body { + pointer-events: none; +} + +svg.block_renderer g.flow_node .input, svg.block_renderer g.flow_node .output { + cursor: pointer; +} + +svg.block_renderer.read-only g.flow_node .input, svg.block_renderer.read-only g.flow_node .output { + cursor: not-allowed; +} + +svg.block_renderer.dragging g.flow_node .input, svg.block_renderer.dragging g.flow_node .output { + cursor: grabbing; +} + +svg.block_renderer g.flow_node rect.port_plating { + fill: #27212e; +} + + +svg.block_renderer g.flow_node circle.external_port.string_port { + fill: $string-color; +} + +svg.block_renderer g.flow_node circle.external_port.integer_port { + fill: $integer-color; +} + +svg.block_renderer g.flow_node circle.external_port.float_port { + fill: $float-color; +} + +svg.block_renderer g.flow_node circle.external_port.boolean_port { + fill: $boolean-color; +} + +svg.block_renderer g.flow_node circle.external_port.pulse_port { + fill: $pulse-color; +} + +svg.block_renderer g.flow_node circle.external_port.user-pulse_port { + fill: url(#user-pulse-pattern) $user-pulse-color-1; +} + +svg.block_renderer g.flow_node circle.external_port.enum_port { + fill: $enum-color; +} + +svg.block_renderer g.flow_node circle.external_port.enum_sequence_port { + fill: $enum-color; +} + +svg.block_renderer g.flow_node circle.external_port.any_port { + fill: $any-color; +} + +svg.block_renderer g.flow_node circle.external_port.list_port { + fill: $list-color; +} + +svg.block_renderer g.flow_node circle.external_port.unknown_type { + fill: $unknown-color; +} + +svg.block_renderer g.flow_node circle.internal_port { + fill: #000000; +} + +svg.block_renderer g.flow_node text { + fill: black; +} + +svg.block_renderer g.flow_node text.node_name { + font-size: 14px; +} + +svg.block_renderer g.flow_node text.argument_name { + font-size: 11px; + font-weight: bold; + fill: #fff; +} + +svg.block_renderer g.flow_node { + // Avoid text selection + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome, Opera and Firefox */ +} + +// Input helpers +svg.block_renderer g.input_helper { + cursor: pointer; +} + +svg.block_renderer.read-only g.input_helper { + cursor: not-allowed; +} + +svg.block_renderer.dragging g.input_helper { + cursor: grabbing; +} + +svg.block_renderer g.input_helper.hidden { + display: none; +} + +svg.block_renderer g.input_helper.pulse_port { // Pulse port input helpers are not yet implemented + display: none; +} + +svg.block_renderer g.input_helper .outer_container{ + fill: #eee; // Same as background + stroke: #222; + stroke-width: 2px; +} + +svg.block_renderer g.input_helper rect { + fill: #444; +} + + +svg.block_renderer g.input_helper path.connection_line { + stroke: #eee; + stroke-width: 2px; + stroke-dasharray: 2px,2px; +} + +// Direct value node +svg.block_renderer g.flow_node.direct_value_node text.node_name { + font-family: helvetica; + cursor: text; +} + +svg.block_renderer.read-only g.flow_node.direct_value_node text.node_name { + cursor: not-allowed; +} + +svg.block_renderer.dragging { + cursor: grabbing; +} + +svg.block_renderer g.flow_node.editing rect.node_body { + stroke-width: 2px; + stroke: #009688; +} + +// Inline editor +#workspace > .inline_editor_container { + position: absolute; +} + +#workspace > .inline_editor_container.hidden { + display: none; +} + +#workspace > .inline_editor_container .hidden { + display: none; +} + +#workspace > .inline_editor_container > input { + font-family: helvetica; + border: none; + background-color: #fff; + color: black; + + // Remove Spinner arrows + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + -moz-appearance: textfield; +} + +// ContentEditable divs +div[contenteditable="true"] { + cursor: text; + width: 100%; + + &.editing { + width: max-content; + } +} + +svg.read-only div[contenteditable="true"] { + cursor: not-allowed; + pointer-events: none; +} + +svg.dragging div[contenteditable="true"] { + cursor: grabbing; +} + +#workspace div[contenteditable="true"] a { + color: revert; + text-decoration: revert; + + &:hover { + color: revert; + text-decoration: revert; + } +} + +div[contenteditable="true"] > div { + width: 100%; +} + +div[contenteditable="true"].editing > div { + padding: 1ex; + border: 1px solid #27212e; + max-width: max-content; +} + +#workspace foreignObject div[contenteditable="true"] font a { + // If an tag is found inside a one respect the 's color. + color: inherit; + + &:hover { + color: inherit; + } +} + +// Floating button bar over fixed text elements +.floating-button-bar { + position: absolute; + z-index: 3; // Less than backdrop + + border-radius: 4px; + box-shadow: 0 0 1px 1px rgba(0,0,0,0.3); + + button { + border-radius: 4px; + border: none; + min-width: 4ex; + min-height: 4ex; + vertical-align: bottom; + + &:hover { + background-color: #27212e; + color: white; + + img.icon { + // Same effect as changing font color, but for images + filter: invert(100%); + } + } + } + + + + .bold-button { + font-weight: bold; + } + + .italic-button { + font-style: italic; + } + + .underline-button { + text-decoration: underline; + } +} + +// Variable dropdown editor +svg.block_renderer .named_var { + cursor: pointer; +} + +svg.block_renderer.read-only .named_var { + cursor: not-allowed; +} + +svg.block_renderer.dragging .named_var { + cursor: grabbing; +} + +svg.block_renderer .named_var .var_name { + font-size: 14px; + fill: black; +} + +svg.block_renderer .named_var .var_plate { + fill: transparent; + stroke-width: 1px; + stroke: #444; +} + +// Popup +.popup_group { + position: absolute; + background-color: #fafafa; + box-shadow: 0 0 4px rgba(0,0,0,0.5); + + z-index: 10; // More than .backdrop + + &.hidden { + display: none; + } + + &.context_menu { + border-radius: 4px; + padding: 4px 0; + + ul { + margin: 0; + padding: 0; + + li { + cursor: pointer; + padding: 0.5ex 1ex; + + &:hover { + color: #fff; + background-color: #009688; + } + } + } + } +} + +.backdrop { + z-index: 5; // More than 0, less than .popup_group + + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0,0,0,0.2); +} + +.popup_group > .editor > input { + border: none; + border-bottom: 2px solid #009688; + padding-left: 1ex; + width: 100%; + min-width: 20ex; +} + +.options { + padding: 0; + margin: 0; + overflow: auto; +} + +.options li { + padding-left: 1ex; + padding-right: 1ex; + cursor: pointer; + + &:hover, &.selected { + background-color: #444; + color: white; + } + + border-bottom: 1px solid rgba(0,0,0,0.3); + padding: 0.5ex 1ex 0.5ex 1ex; +} + +// Trashcan +.trashcan.invisible { + visibility: invisible; +} + +.trashcan .backdrop { + fill: #fafafa; +} + +.trashcan .backdrop_shadow { + // Separated in a different element to avoid problems + // in case filter problems make it disappear + filter: url("#shadow"); + fill: #000; // Backdrop color +} + +.trashcan.to-be-activated .backdrop { + fill: #ff4444; +} + +// Floating buttons +svg.block_renderer .fab-button-group { + .button { + cursor: pointer; + } + + .button-body { + fill: #27212e; + } + + .button-shadow { + // Separated in a different element to avoid problems + // in case filter problems make it disappear + filter: url("#shadow"); + fill: rgba(0,0,0,0.3); // Shadow color + } + + .button-symbol { + stroke: #fff; + stroke-width: 2px; + } +} + +// Toolbox +.toolbox { + background-color: rgba(0,0,0,0.3); + position: absolute; + left: 0; + display: flex; + + + &.landscape { + height: 100%; + min-width: 5ex; + max-width: min(30vw, 30rem); + + padding-left: 6rem; // Space for the shortcut list + top: 0; + + .hide-button-section { + display: none; + } + } + + &.portrait { + width: 100%; + max-height: min(30vh, 30rem); + min-height: 2.2rem; + + padding-left: 0; + top: calc(100% - min(30vh, 30rem)); + + &.collapsed { + height: 2.2rem; + top: calc(100% - 2.2rem); + } + + .hide-button-section { + position: absolute; + right: 0.5rem; + } + } +} + +.toolbox .hide-button-section button { + border: none; + background-color: #009688; + color: white; + padding-bottom: 0.5ex; + font-weight: bolder; + border-radius: 4px; +} + + +.toolbox.collapsed { + .showcase { + visibility: hidden; + } + + .hide-button-section { + display: none; + } +} + +.toolbox.subsumed { + pointer-events: none; + opacity: 0.2; +} + +.toolbox .category-shortcut-list { + margin: 0; + padding: 0; + width: 6rem; + + position: absolute; + top: 0; + left: 0; + + height: inherit; + max-height: inherit; + max-width: inherit; + + overflow-y: auto; + background-color: #0f110e; + + .contents { + padding: 0; + margin: 0; + + li { + cursor: pointer; + color: white; + text-align: center; + margin: 0.5ex 2px 0.5ex 2px; + background-color: #262826; + border-radius: 4px; + padding: 4px; + font-size: small; + word-wrap: break-word; + user-select: none; + } + } +} + +.toolbox.portrait { + .category-shortcut-list { + top: calc(100% - 2.2rem); + height: 2.2rem; + width: 100%; + overflow-x: auto; + overflow-y: hidden; + + .contents { + width: max-content; + + li { + display: inline-block; + } + } + } + + &.collapsed .category-shortcut-list { + top: 0; // Nothing more on the section + } + + .showcase { + overflow-x: auto; + + .category { + overflow-x: auto; + + .content { + width: max-content; + min-width: 100%; + + .block_exhibitor { + display: inline-block; + } + } + } + } +} + +.toolbox-flow-button { + margin: 1ex; + padding: 1ex; + background: #009688; + color: #fff; + border: none; + font-weight: 500; + box-shadow: 0 0 1px 1px rgba(0,0,0,0.3); +} + +.toolbox .category.empty { + display: none; +} + +.toolbox .category-shortcut-list .contents li.empty { + display: none; +} + +.category .category_title { + background-color: #27212e; + padding: 0.5ex; + font-weight: bold; + width: 100%; + color: white; + cursor: pointer; +} + +.toolbox .showcase { + width: max-content; + overflow-x: hidden; + overflow-y: auto; +} + +.toolbox .block_exhibitor { + margin: 1ex; +} + +.toolbox .category.collapsed .block_exhibitor { + display: none; +} + +.toolbox .category.collapsed .category_title { + border-bottom: 1px solid #444; +} + +.toolbox.subsumed .block_exhibitor { + pointer-events: none; +} + +.toolbox.subsumed svg g.flow_node > a { + pointer-events: none; +} + +.block_exhibitor.hidden g.flow_node { + visibility: hidden; +} + +.block_exhibitor { + display: block; +} + +// Block internal wiring +.var_connector { + stroke-width: 4px; + stroke: $any-color; + + &.string_port { + stroke: $string-color; + } + + &.integer_port { + stroke: $integer-color; + } + + &.float_port { + stroke: $float-color; + } + + &.boolean_port { + stroke: $boolean-color; + } + + &.enum_port { + stroke: $enum-color; + } + + &.enum_sequence_port { + stroke: $enum-color; + } + + &.any_port { + stroke: $any-color; + } + + &.list_port { + stroke: $list-color; + } + + &.pulse_port { + stroke: $pulse-color; + } + + &.unknown_port { + stroke: $unknown-color; + } +} + + +path.var_path { + stroke-width: 3px; + stroke: $any-color; + stroke-dasharray: 5; + + &.string_port { + stroke: $string-color; + } + + &.integer_port { + stroke: $integer-color; + } + + &.float_port { + stroke: $float-color; + } + + &.boolean_port { + stroke: $boolean-color; + } + + &.enum_port { + stroke: $enum-color; + } + + &.enum_sequence_port { + stroke: $enum-color; + } + + &.any_port { + stroke: $any-color; + } + + &.list_port { + stroke: $list-color; + } + + &.pulse_port { + stroke: $pulse-color; + } + + &.unknown_port { + stroke: $unknown-color; + } +} + +// Node icon +.node_icon_plate { + fill: #e7e7e7; +} + +.node_icon, .node_icon_separator, .node_icon_plate { + pointer-events: none; +} + +// UI nodes +svg.block_renderer g.flow_node.button_node .node_body { + fill: #EEE; + stroke-width: 1; + stroke: #BBB; +} + +svg.block_renderer g.flow_node.output_node .node_body { + fill: #222; +} + +svg.block_renderer g.flow_node.output_node .output_text { + fill: #fc4; +} + +svg.block_renderer g.flow_node.image_node image { + pointer-events: none; +} + +svg.block_renderer g.flow_node.image_node .node_body { + fill: rgba(255,255,255,0.3) +} + +svg.block_renderer g.flow_node.separator_node { + .representation { + stroke: #000; + mix-blend-mode: difference; + stroke-width: 2px; + } + + .node_body { + fill: rgba(255,255,255,0.3); + } +} + +svg.block_renderer g.flow_node.container_node .node_body { + stroke-width: 1; + stroke-dasharray: 5; + stroke: rgba(0,0,0,0.5); +} + +svg.block_renderer g.flow_node.container_node.simple_card .node_body { + stroke-width: 0; +} + +svg.block_renderer g.flow_node.selected.container_node.simple_card .node_body { + stroke-width: 2; + stroke-dasharray: 10; +} + + +svg.block_renderer g.flow_node.container_node.highlighted .node_body { + fill: #AFA !important; +} + +svg.block_renderer g.flow_node.section_node .node_body { + fill: rgba(255,255,255,0.5); +} + +svg.block_renderer g.flow_node.action_area .node_body { + fill: rgba(255,255,255,0.5); + stroke-dasharray: 5; +} + +svg.block_renderer g.flow_node.text_node .node_body { + fill: rgba(255,255,255,0.5); +} + +svg.block_renderer g.flow_node.text_box .node_body { + fill: #fff; + stroke-width: 2; + stroke: #444; +} + +// Don't use transparency on showcase +.showcase svg.block_renderer g.flow_node.text_node .node_body { + fill: #fff; +} + +.showcase svg.block_renderer g.flow_node.section_node .node_body { + fill: #fff; +} + +svg.block_renderer g.flow_node.container_node.responsive_page { + + .node_body { + cursor: auto; + + } + + rect { + pointer-events: none; + } + + rect.titlebox, text { + pointer-events: all; + cursor: text + } +} + +svg.block_renderer.dragging g.flow_node.container_node.responsive_page { + .node_body { + cursor: grabbing; + } + rect.titlebox, text { + cursor: grabbing; + } +} + + +svg.block_renderer g.flow_node.container_node.responsive_page .output_text { + fill: #fff; + font-style: normal; +} + +svg.block_renderer g.flow_node.container_node .output_text { + fill: #222; + font-style: italic; +} + +svg.block_renderer g.flow_node .manipulators .manipulator path { + fill: transparent; + stroke: #ff0; + stroke-width: 2px; +} + +svg.block_renderer g.flow_node .manipulators .manipulator.hidden { + display: none; +} + +svg.block_renderer g.flow_node .manipulators .manipulator .handle-icon-background { + fill: #222; +} + +svg.block_renderer g.flow_node .manipulators .resize-manipulator { + cursor: se-resize; +} + +svg.block_renderer g.flow_node .manipulators .height-resizer.resize-manipulator { + cursor: s-resize; +} + +svg.block_renderer g.flow_node .manipulators .width-resizer.resize-manipulator { + cursor: e-resize; +} +svg.block_renderer.dragging g.flow_node .manipulators .resize-manipulator { + cursor: grabbing; +} + +svg.block_renderer g.flow_node .manipulators .manipulator.settings-manipulator image { + cursor: pointer; + filter: invert(100%); +} + +svg.block_renderer g.flow_node .manipulators.hidden { + display: none; +} diff --git a/frontend/src/index.html b/frontend/src/index.html index d18bf8b7..72ff9252 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -12,21 +12,98 @@ -
+
+
+ +

Loading PrograMaker...

+ +
+ + +
+
+
diff --git a/frontend/src/main.server.ts b/frontend/src/main.server.ts new file mode 100644 index 00000000..10150a71 --- /dev/null +++ b/frontend/src/main.server.ts @@ -0,0 +1,10 @@ +import { enableProdMode } from '@angular/core'; + +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +export { AppServerModule } from './app/app.server.module'; +export { renderModule, renderModuleFactory } from '@angular/platform-server'; diff --git a/frontend/src/main.ts b/frontend/src/main.ts index a9ca1caf..fdc8b233 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,11 +1,13 @@ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; +import { AppBrowserModule } from './app/app.browser.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule); +document.addEventListener('DOMContentLoaded', () => { + platformBrowserDynamic().bootstrapModule(AppBrowserModule); +}); diff --git a/frontend/src/spreadsheet_editor.scss b/frontend/src/spreadsheet_editor.scss new file mode 100644 index 00000000..c27b59a7 --- /dev/null +++ b/frontend/src/spreadsheet_editor.scss @@ -0,0 +1,5 @@ +.spreadsheet-table { + .mat-tab-label { + height: 2.5em; + } +} diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index a06bc291..0ade825f 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,13 +1,25 @@ /* You can add global styles to this file, and also import other style files */ @import 'material_theme'; @import 'blockly_theme'; +@import './flow_editor'; +@import './spreadsheet_editor'; -html, body, my-app, mat-sidenav-container, .app-content { +html, body, my-app, .app-content { margin: 0; width: 100%; height: 100%; } +html, body { + /* Block refresh on scroll. */ + overscroll-behavior-y: contain; +} + +mat-sidenav-container { + margin: 0; + width: 100%; +} + body { background-color: #fafafa; } @@ -16,12 +28,49 @@ body { height: auto; } +img[role="avatar"] { + object-fit: scale-down; + // @COMPATIBILITY@ Note that this is not supported on IE, in that case the + // avatar will be stretched, which is a reasonable result. + + // Another alternative would be to set 'max-width' and 'max-height' to the + // desired image size, and 'width' and 'height' to auto. +} + +.mat-drawer-inner-container { + padding-top: 1em; +} + +.mat-drawer-inner-container > ul { + padding-left: 0.5ex; + padding-right: 1ex; // Compensate badges (not accounted for when laying-out) +} + +.mat-drawer-inner-container > ul > li > button { + width: 100%; + font-size: 150%; + padding-top: 1ex; + padding-bottom: 1ex; + text-align: right; +} + mat-sidenav, .full-height { height: 100%; } mat-sidenav.mat-sidenav { - background-color: #F5F5F5; + background-color: #FFFFFF; + box-shadow: 2px 0px 3px 0px rgba(0,0,0,0.3); +} + +.mat-drawer-inner-container > ul { + border-bottom: 1px solid #888; + margin-bottom: 0; + padding-bottom: 1rem; +} + +.mat-drawer-inner-container > ul:last-child { + border-bottom: none; } mat-toolbar-row { @@ -95,6 +144,10 @@ a[role="button"], a[role="button"]:not([href]), a[role="button"]:not([href]):hov cursor: pointer; } +mat-card[role="button"], .mat-card[role="button"] { + cursor: pointer; +} + .login-form { margin-top: 2em; max-width: 40em; @@ -121,9 +174,15 @@ a[role="button"], a[role="button"]:not([href]), a[role="button"]:not([href]):hov text-align: center; } -.call-to-action { - background-color: orange !important; - color: white !important; +mat-card.call-to-action, div.call-to-action, button.call-to-action { + background-color: orange; + color: white; +} + +.call-to-action.soft { + background-color: #fc8; + color: #000; + border-radius: 4px; } mat-card.call-to-action > h4 { @@ -206,6 +265,7 @@ mat-card.call-to-action > h4 { /* How to enable services */ div.console { padding: 1ex; + background-color: #222; color: #eee; border-radius: 2px; @@ -241,3 +301,155 @@ mat-card.bridge.connected-false { .full-dialog-size { width: 80vw; } + +body .mat-dialog-container { + max-height: 100vh; +} + +/* Logs panel */ +#logs_panel_container > .log-entry > button { + float: right; +} + +#logs_panel_container > .log-entry > .time { + font-size: small; + display: block; +} + +@keyframes glow-new-log-entry { + from {background-color: rgba(255, 100, 100, 1);} + to {background-color: rgba(255, 100, 100, 0);} +} + +#logs_panel_container > .log-entry { + border-bottom: 1px dashed rgba(0,0,0,0.3); + padding: 1ex 1em 1ex 1em; + animation-name: glow-new-log-entry; + animation-duration: 2s; +} + +/* Variables panel */ +#variables_panel_container table.var-table { + .to-delete { + background: repeating-linear-gradient( 45deg, #ddd, #ddd 10px, #fff 10px, #fff 20px ); + } + + td { + vertical-align: baseline; + + .mat-form-field { + + .mat-form-field-wrapper { + padding: 0; + + .mat-form-field-underline { + bottom: 0; + } + } + } + + &.name { + text-align: center; + } + + &.value { + .snippet { + max-height: 75vh; + } + } + } +} + +.invisible { + visibility: hidden; +} + + +/* Program detail upload button animation */ +@keyframes button-load-started { + from {width: 100%;} + to {width: 50%;} +} +@keyframes button-load-completed { + from {width: 50%;} + to {width: 0%;} +} + +button > .mat-button-wrapper > .load-bg { + background-color: #27212e; + height: 100%; + position: absolute; + z-index: -1; + right: 0px; + top: 0; +} + +button.mat-button .load-bg { + border-radius: 4px; +} + +button.started > span > .load-bg { + animation-name: button-load-started; + animation-duration: 0.2s; +} + +button.completed > span > .load-bg { + animation-name: button-load-completed; + animation-duration: 0.3s; +} +/* Annotated buttons */ +.annotated-icon > .mat-button-wrapper { + display: grid; +} + +.annotated-icon mat-icon { + grid-row: 1; + grid-column: 1; + text-align: center; + width: 100%; +} + +.annotated-icon .icon { + grid-row: 1; + grid-column: 1; + margin: 0 auto; + height: 1.5rem; + pointer-events: none; +} + +.annotated-icon:disabled .icon { + filter: contrast(1%); +} + +.annotated-icon .label { + grid-row: 2; + grid-column: 1; + font-size: small; + line-height: initial; + margin: 0; + width: 100%; + text-align: center; + margin-bottom: 0px; +} + +/* Settings */ +.toggle-setting > mat-slide-toggle > label { + position: relative; +} + +.toggle-setting > mat-slide-toggle > label > .mat-slide-toggle-content { + position: absolute; + top: 100%; + left: -25%; + font-size: small; +} + +/* Tab management */ +div.mat-tab-body-content { + overflow: hidden; +} + +// Don't show tab-bar when only a group's profile is shown (and no other tab) +.profile-section.profile-only > mat-tab-group > mat-tab-header { + display: none; +} diff --git a/frontend/src/tsconfig.app.json b/frontend/src/tsconfig.app.json index fa866585..8bdc4beb 100644 --- a/frontend/src/tsconfig.app.json +++ b/frontend/src/tsconfig.app.json @@ -1,9 +1,11 @@ { "compilerOptions": { + "allowSyntheticDefaultImports": true, "downlevelIteration": true, "sourceMap": true, "declaration": false, "moduleResolution": "node", + "noImplicitAny": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "lib": [ @@ -12,9 +14,11 @@ ], "outDir": "../out-tsc/app", "target": "es2015", - "module": "esnext", + "module": "es2020", "baseUrl": "", - "types": [] + "types": [ + "node" + ] }, "exclude": [ "test.ts", diff --git a/frontend/src/tsconfig.server.json b/frontend/src/tsconfig.server.json new file mode 100644 index 00000000..621224a6 --- /dev/null +++ b/frontend/src/tsconfig.server.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../out-tsc/server", + "target": "es2016", + "types": [ + "node" + ] + }, + "files": [ + "./main.server.ts", + "../server.ts" + ], + "angularCompilerOptions": { + "entryModule": "./app/app.server.module#AppServerModule" + } +} diff --git a/frontend/src/tsconfig.spec.json b/frontend/src/tsconfig.spec.json index f6c40a29..56a4134b 100644 --- a/frontend/src/tsconfig.spec.json +++ b/frontend/src/tsconfig.spec.json @@ -4,13 +4,15 @@ "sourceMap": true, "declaration": false, "moduleResolution": "node", + "noImplicitAny": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "lib": [ + "dom", "es2016" ], "outDir": "../out-tsc/spec", - "module": "esnext", + "module": "es2020", "target": "es2015", "baseUrl": "", "types": [ diff --git a/frontend/tsconfig.base.json b/frontend/tsconfig.base.json new file mode 100644 index 00000000..f39ffcfe --- /dev/null +++ b/frontend/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "downlevelIteration": true, + "module": "commonjs", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2016", + "dom" + ], + "target": "es2015" + } +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 3a2a7905..c87d836d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,17 +1,20 @@ +/* + This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. + It is not intended to be used to perform a compilation. + + To learn more about this file see: https://angular.io/config/solution-tsconfig. +*/ { - "compileOnSave": false, - "compilerOptions": { - "downlevelIteration": true, - "module": "esnext", - "outDir": "./dist/out-tsc", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "lib": [ - "es2016", - "dom", - ], - "target": "es2015" -} + "files": [], + "references": [ + { + "path": "./src/tsconfig.app.json" + }, + { + "path": "./src/tsconfig.spec.json" + }, + { + "path": "./src/tsconfig.server.json" + } + ] +} \ No newline at end of file diff --git a/frontend/tslint.json b/frontend/tslint.json index 17016304..79ea4792 100644 --- a/frontend/tslint.json +++ b/frontend/tslint.json @@ -10,6 +10,9 @@ "check-space" ], "curly": true, + "deprecation": { + "severity": "warning" + }, "eofline": true, "forin": true, "import-blacklist": [true], @@ -53,7 +56,6 @@ "no-switch-case-fall-through": true, "no-trailing-whitespace": true, "no-unused-expression": true, - "no-use-before-declare": true, "no-var-keyword": true, "object-literal-sort-keys": false, "one-line": [ diff --git a/utils/build-and-push-to-dockerhub.sh b/utils/build-and-push-to-dockerhub.sh index 6b5938c2..bfc9885d 100755 --- a/utils/build-and-push-to-dockerhub.sh +++ b/utils/build-and-push-to-dockerhub.sh @@ -6,11 +6,10 @@ cd "$(dirname "$0")" # Frontend cd ../frontend -docker build . -t plazaproject/plaza-core-frontend:`git rev-parse HEAD` -docker push plazaproject/plaza-core-frontend:`git rev-parse HEAD` +docker build . -t programakerproject/programaker-core-frontend:`git rev-parse HEAD` +docker push programakerproject/programaker-core-frontend:`git rev-parse HEAD` # Backend cd ../backend -docker build . -t plazaproject/plaza-core-backend:`git rev-parse HEAD` -docker push plazaproject/plaza-core-backend:`git rev-parse HEAD` - +docker build . -t programakerproject/programaker-core-backend:`git rev-parse HEAD` +docker push programakerproject/programaker-core-backend:`git rev-parse HEAD` diff --git a/utils/build-backend-hotfix.sh b/utils/build-backend-hotfix.sh new file mode 100755 index 00000000..7f66bfe4 --- /dev/null +++ b/utils/build-backend-hotfix.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -eu + +cd "$(dirname "$0")/../backend" + +TAG_PREFIX=${TAG_PREFIX:-hotfix} +TAG=`date +%Y%m%d-%H%M%S` +PRE_BUILD=backend-pre-$TAG_PREFIX-$TAG + +docker build -t $PRE_BUILD -f scripts/ci-partial.dockerfile . + +sh ../utils/ci-preparations/optimize-backend-image.sh $PRE_BUILD registry.gitlab.com/programaker-project/programaker-core/backend:$TAG_PREFIX-$TAG + +echo "docker push registry.gitlab.com/programaker-project/programaker-core/backend:$TAG_PREFIX-$TAG" diff --git a/utils/ci-preparations/optimize-backend-image.sh b/utils/ci-preparations/optimize-backend-image.sh index 18624d5b..a1a04bcc 100644 --- a/utils/ci-preparations/optimize-backend-image.sh +++ b/utils/ci-preparations/optimize-backend-image.sh @@ -5,10 +5,13 @@ set -eu SOURCE_IMAGE="$1" DESTINATION_IMAGE="$2" TMP_FILE="${3:-OPTIMIZED_IMAGE.dockerfile}" -[ ! -f "${TMP_FILE}" ] # Check that TMP_FILE does not exist +if [ -f "${TMP_FILE}" ];then # Check that TMP_FILE does not exist + echo "Unfinished build still on progress? Check $TMP_FILE" + exit 1 +fi # Check base image of erlang:alpine -TEMPLATE="FROM alpine:3.9 as final +TEMPLATE="FROM alpine:3.12 as final RUN apk add ncurses libstdc++ erlang diff --git a/utils/ci-preparations/prepare-backend-ci.sh b/utils/ci-preparations/prepare-backend-ci.sh index 9d418aa7..a744a8bf 100755 --- a/utils/ci-preparations/prepare-backend-ci.sh +++ b/utils/ci-preparations/prepare-backend-ci.sh @@ -8,10 +8,10 @@ cd ../../backend TAG=`git rev-parse HEAD` -LOCAL_NAME="plaza-backend-ci-base-preparation:$TAG" -REMOTE_NAME="plazaproject/ci-base-backend:$TAG" +LOCAL_NAME="programaker-backend-ci-base-preparation:$TAG" +REMOTE_NAME="programakerproject/ci-base-backend:$TAG" -docker build --no-cache -t "${LOCAL_NAME}" --target plaza-backend-ci-base . +docker build --no-cache -t "${LOCAL_NAME}" --target programaker-backend-ci-base . docker tag "${LOCAL_NAME}" "${REMOTE_NAME}" diff --git a/utils/ci-preparations/prepare-frontend-ci.sh b/utils/ci-preparations/prepare-frontend-ci.sh index 770d443b..8dc62599 100755 --- a/utils/ci-preparations/prepare-frontend-ci.sh +++ b/utils/ci-preparations/prepare-frontend-ci.sh @@ -8,11 +8,33 @@ cd ../../frontend TAG=`git rev-parse HEAD` -LOCAL_NAME="plaza-frontend-ci-base-preparation:$TAG" -REMOTE_NAME="plazaproject/ci-base-frontend:$TAG" +LOCAL_NAME="programaker-frontend-ci-base-preparation:$TAG" +REMOTE_NAME="programakerproject/ci-base-frontend:$TAG" + +BROWSER_LOCAL_NAME="programaker-frontend-ci-base-preparation-browser:$TAG" +BROWSER_REMOTE_NAME="programakerproject/ci-base-frontend-browser:$TAG" + +set -x docker build --no-cache -t "${LOCAL_NAME}" --target ci-base . docker tag "${LOCAL_NAME}" "${REMOTE_NAME}" -echo "Preparation ready, push the image with: docker push '${REMOTE_NAME}'" +BROWSER_PARTIAL=scripts/frontend-browser-ci-partial.dockerfile +echo "FROM ${REMOTE_NAME}" > "${BROWSER_PARTIAL}" +cat >> "${BROWSER_PARTIAL}" < " + exit 1 +fi + +set -eu -o pipefail -o functrace + +make + +CLI=plaza-testbench-master/cli/cli + +clean_and_exit(){ + trap - SIGINT SIGTERM ERR EXIT + + cleanup + + exit 1 +} + +failure() { + local lineno=$1 + local msg=$2 + echo "[ERROR] Failed at $lineno: $msg" +} + +cleanup() { + trap - SIGINT SIGTERM ERR EXIT + + echo "==> Cleaning up" + set +eu + set -x + + docker rm -f "back-$TESTID" + docker rm -f "front-$TESTID" +} + +trap clean_and_exit SIGINT SIGTERM EXIT +trap 'failure ${LINENO} "$BASH_COMMAND"' ERR + +TESTID="$RANDOM$RANDOM$RANDOM$RANDOM" + + +case "${CI_TYPE:-}" in + gitlab) + # Consider: https://stackoverflow.com/a/54252215 + export BACK_DOCKER=`docker run -d --name="back-$TESTID" --rm -p 8888:8888 "$BACKIMG"` + export FRONT_DOCKER=`docker run -d --name="front-$TESTID" --link="$BACK_DOCKER":plaza-backend -p 80:80 --rm "$FRONTIMG"` + BACK_HOST="docker" # Given by DockerInDocker + FRONT_HOST="docker" # Given by DockerInDocker + FRONT_PORT=80 + ;; + + *) + # Expect local execution + export BACK_DOCKER=`docker run -d --name="back-$TESTID" --rm "$BACKIMG"` + export FRONT_DOCKER=`docker run -d --name="front-$TESTID" --link="$BACK_DOCKER":plaza-backend --rm "$FRONTIMG"` + BACK_HOST=`docker exec "$BACK_DOCKER" hostname -i|tr -d '\r\n'` + FRONT_HOST=`docker exec "$FRONT_DOCKER" hostname -i|tr -d '\r\n'` + FRONT_PORT=80 +esac + +# xfce4-terminal -H -e "docker logs -f $FRONT_DOCKER" & + +# Re-wire the back's 80 to 8888 to support the default programaker front configuration +docker exec "$BACK_DOCKER" sh -c 'apk add socat && socat tcp-listen:80,reuseaddr,fork tcp-connect:127.0.0.1:8888' & + +export PLAZA_ROOT="http://$BACK_HOST:8888" + +for i in `seq 1 60`;do + curl -s "$PLAZA_ROOT"/api/v0/ping >> /dev/null && break || sleep 1 +done + + +USERNAME=test_$TESTID +PASSWD=test_$TESTID + +$CLI register -name $USERNAME -password $PASSWD -email $USERNAME@test.com +$CLI login -name $USERNAME -password $PASSWD +TOKEN=`$CLI login -name $USERNAME -password $PASSWD|jq -r .token` + +echo "=> Token: $TOKEN" + +export FRONT="http://$FRONT_HOST:$FRONT_PORT" + +LOGGED_SETTINGS=`links -receive-timeout 10 -retries 1 -dump "$FRONT/settings" -http.extra-header "Cookie: programaker-auth=$TOKEN"` +echo -e "=> Logged settings\n-------------\n$LOGGED_SETTINGS\n-------------\n\n\n" + +ERRONEOUS_SETTINGS=`links -receive-timeout 10 -retries 1 -dump "$FRONT/settings" -http.extra-header "Cookie: programaker-auth=A-NON-EXISTING-TOKEN"` +echo -e "=> Token error settings\n-------------\n$ERRONEOUS_SETTINGS\n-------------\n\n\n" + +ANONYMOUS_SETTINGS=`links -receive-timeout 10 -retries 1 -dump "$FRONT/settings"` +echo -e "=> Anonymous settings\n-------------\n$ANONYMOUS_SETTINGS\n-------------\n\n\n" + + +assert_unexpected_expected() { + # $1 text + # $2 Unexpected + # $3 Expected + if echo "$1" | grep -q "$2" ;then + echo -e "Error: Found an unexpected '$2' in\n\n-------------\n$1\n" 1>&2 + exit 1 + else + if echo "$1" | grep -q "$3" ;then + true + else + echo -e "Error: Not found the expected '$3' in\n\n-------------\n$1\n" 1>&2 + exit 1 + fi + fi +} + +assert_logged() { + assert_unexpected_expected "$1" Login 'Profile info' +} + +assert_anonymous() { + assert_unexpected_expected "$1" 'Profile info' Login +} + +assert_logged "$LOGGED_SETTINGS" +assert_anonymous "$ERRONEOUS_SETTINGS" +assert_anonymous "$ANONYMOUS_SETTINGS" + +echo "=> Completed successfully" + + +cleanup diff --git a/utils/monitoring/blackbox-exporter/Dockerfile b/utils/monitoring/blackbox-exporter/Dockerfile index 58ba3376..ea0d1dae 100644 --- a/utils/monitoring/blackbox-exporter/Dockerfile +++ b/utils/monitoring/blackbox-exporter/Dockerfile @@ -2,8 +2,8 @@ FROM alpine:latest RUN apk add ca-certificates -ADD https://github.com/prometheus/blackbox_exporter/releases/download/v0.14.0/blackbox_exporter-0.14.0.linux-amd64.tar.gz blackbox_exporter.tar.gz -RUN tar xvf blackbox_exporter.tar.gz && mv -v blackbox_exporter-0.14.0.linux-amd64/blackbox_exporter /usr/bin/blackbox_exporter && rm -Rfv blackbox_exporter-0.14.0.linux-amd64 blackbox_exporter.tar.gz +ADD https://github.com/prometheus/blackbox_exporter/releases/download/v0.18.0/blackbox_exporter-0.18.0.linux-amd64.tar.gz blackbox_exporter.tar.gz +RUN tar xvf blackbox_exporter.tar.gz && mv -v blackbox_exporter-0.18.0.linux-amd64/blackbox_exporter /usr/bin/blackbox_exporter && rm -Rfv blackbox_exporter-0.18.0.linux-amd64 blackbox_exporter.tar.gz ADD config.yml /etc/blackbox_exporter/config.yml ENTRYPOINT [ "blackbox_exporter" ] diff --git a/utils/monitoring/docker-compose.yml b/utils/monitoring/docker-compose.yml index d1863bbf..f3b272db 100644 --- a/utils/monitoring/docker-compose.yml +++ b/utils/monitoring/docker-compose.yml @@ -4,17 +4,28 @@ version: '3' services: blackbox-exporter: build: blackbox-exporter + restart: always prometheus: build: prometheus + restart: always links: - blackbox-exporter ports: - 9090:9090 + volumes: + - programaker-monitoring-prometheus:/prometheus grafana: build: grafana + restart: always ports: - 3000:3000 links: - prometheus + volumes: + - programaker-monitoring-grafana:/var/lib/grafana + +volumes: + programaker-monitoring-grafana: + programaker-monitoring-prometheus: diff --git a/utils/monitoring/grafana/Dockerfile b/utils/monitoring/grafana/Dockerfile index 5a9a446b..e457e0f9 100644 --- a/utils/monitoring/grafana/Dockerfile +++ b/utils/monitoring/grafana/Dockerfile @@ -1,3 +1,3 @@ -FROM grafana/grafana:6.1.3 +FROM grafana/grafana:7.3.7 -ADD config/plaza-dashboard.json /usr/share/grafana/public/dashboards/plaza.json +ADD config/programaker-dashboard.json /usr/share/grafana/public/dashboards/programaker.json diff --git a/utils/monitoring/grafana/config/plaza-dashboard.json b/utils/monitoring/grafana/config/programaker-dashboard.json similarity index 61% rename from utils/monitoring/grafana/config/plaza-dashboard.json rename to utils/monitoring/grafana/config/programaker-dashboard.json index b4d36d3e..d9576dd9 100644 --- a/utils/monitoring/grafana/config/plaza-dashboard.json +++ b/utils/monitoring/grafana/config/programaker-dashboard.json @@ -10,11 +10,17 @@ } ], "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, { "type": "grafana", "id": "grafana", "name": "Grafana", - "version": "6.1.3" + "version": "7.3.7" }, { "type": "panel", @@ -33,6 +39,12 @@ "id": "singlestat", "name": "Singlestat", "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" } ], "annotations": { @@ -52,25 +64,14 @@ "gnetId": null, "graphTooltip": 0, "id": null, - "iteration": 1560978507369, + "iteration": 1613819147127, "links": [], "panels": [ - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 22, - "panels": [], - "title": "Services", - "type": "row" - }, { "cacheTimeout": null, "colorBackground": true, + "colorPostfix": false, + "colorPrefix": false, "colorValue": false, "colors": [ "#6d1f62", @@ -79,6 +80,12 @@ ], "datasource": "${DS_PROMETHEUS}", "description": "Storage supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -88,10 +95,10 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 0, - "y": 1 + "y": 0 }, "id": 24, "interval": null, @@ -110,6 +117,7 @@ "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, + "pluginVersion": "6.1.3", "postfix": "", "postfixFontSize": "50%", "prefix": "", @@ -130,15 +138,18 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_storage_sup\"}", + "expr": "min(automate_service{name=\"automate_storage\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "0,0.5", + "timeFrom": null, + "timeShift": null, "title": "Storage", "type": "singlestat", "valueFontSize": "80%", @@ -171,7 +182,13 @@ "#299c46" ], "datasource": "${DS_PROMETHEUS}", - "description": "Bridge engine supervisor", + "description": "Bot engine supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -181,12 +198,12 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 4, - "y": 1 + "y": 0 }, - "id": 55, + "id": 34, "interval": null, "links": [], "mappingType": 1, @@ -214,6 +231,8 @@ "to": "null" } ], + "repeat": null, + "repeatDirection": "h", "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, @@ -223,16 +242,17 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_service_port_engine_sup\"}", + "expr": "min(automate_service{name=\"automate_bot_engine\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "0,0.5", - "title": "Bridge engine", + "title": "Bot engine", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -252,7 +272,7 @@ "value": "1" } ], - "valueName": "current" + "valueName": "avg" }, { "cacheTimeout": null, @@ -264,7 +284,13 @@ "#299c46" ], "datasource": "${DS_PROMETHEUS}", - "description": "Bot engine supervisor", + "description": "Bridge engine supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -274,12 +300,12 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 8, - "y": 1 + "y": 0 }, - "id": 34, + "id": 55, "interval": null, "links": [], "mappingType": 1, @@ -307,8 +333,6 @@ "to": "null" } ], - "repeat": null, - "repeatDirection": "h", "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, @@ -318,16 +342,17 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_bot_engine_sup\"}", + "expr": "min(automate_service{name=\"automate_service_port_engine\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "0,0.5", - "title": "Bot engine", + "title": "Bridge engine", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -347,67 +372,39 @@ "value": "1" } ], - "valueName": "avg" + "valueName": "current" }, { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 0.5 - ], - "type": "gt" - }, - "operator": { - "type": "and" - }, - "query": { - "params": [ - "A", - "10s", - "now" - ] - }, - "reducer": { - "params": [], - "type": "min" - }, - "type": "query" - } - ], - "executionErrorState": "alerting", - "for": "0m", - "frequency": "60s", - "handler": 1, - "message": "Failing plaza service", - "name": "Plaza services alert", - "noDataState": "alerting", - "notifications": [ - { - "id": 1 - } - ] - }, "aliasColors": {}, "bars": false, + "cacheTimeout": null, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 4, - "w": 8, - "x": 16, - "y": 1 + "h": 5, + "w": 12, + "x": 12, + "y": 0 }, - "id": 37, + "hiddenSeries": false, + "id": 71, + "interval": "", "legend": { "avg": false, "current": false, "max": false, "min": false, - "show": true, + "show": false, "total": false, "values": false }, @@ -415,9 +412,13 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, - "pointradius": 0.5, - "points": true, + "pluginVersion": "7.3.7", + "pointradius": 2, + "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, @@ -425,26 +426,40 @@ "steppedLine": false, "targets": [ { - "expr": "count(automate_service) - sum(automate_service)", + "expr": "min(automate_logged_users_last_hour)", "format": "time_series", + "interval": "", "intervalFactor": 1, - "legendFormat": "", + "legendFormat": "People logged last hour", "refId": "A" } ], - "thresholds": [ + "thresholds": [], + "timeFrom": null, + "timeRegions": [ { - "colorMode": "critical", - "fill": true, + "colorMode": "blue", + "fill": false, + "fillColor": "rgba(234, 112, 112, 0.12)", + "from": "00", "line": true, - "op": "gt", - "value": 0.5 + "lineColor": "rgba(237, 46, 24, 0.60)", + "op": "time", + "to": "08" + }, + { + "colorMode": "yellow", + "fill": false, + "fillColor": "rgba(234, 112, 112, 0.12)", + "from": "09", + "line": true, + "lineColor": "rgba(237, 46, 24, 0.60)", + "op": "time", + "to": "18" } ], - "timeFrom": null, - "timeRegions": [], "timeShift": null, - "title": "Failing services", + "title": "Active (Logged last hour)", "tooltip": { "shared": true, "sort": 0, @@ -460,6 +475,7 @@ }, "yaxes": [ { + "$$hashKey": "object:303", "format": "short", "label": null, "logBase": 1, @@ -468,6 +484,7 @@ "show": true }, { + "$$hashKey": "object:304", "format": "short", "label": null, "logBase": 1, @@ -492,6 +509,12 @@ ], "datasource": "${DS_PROMETHEUS}", "description": "Service registry supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -501,7 +524,7 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 0, "y": 2 @@ -543,9 +566,10 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_service_registry_sup\"}", + "expr": "min(automate_service{name=\"automate_service_registry\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" @@ -584,7 +608,13 @@ "#299c46" ], "datasource": "${DS_PROMETHEUS}", - "description": "Channel engine supervisor", + "description": "Bot engine runner supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -594,12 +624,12 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 4, "y": 2 }, - "id": 33, + "id": 35, "interval": null, "links": [], "mappingType": 1, @@ -636,16 +666,17 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_channel_engine_sup\"}", + "expr": "min(automate_service{name=\"automate_bot_engine_program_runner\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "0,0.5", - "title": "Channel engine", + "title": "Bot engine runner", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -677,7 +708,13 @@ "#299c46" ], "datasource": "${DS_PROMETHEUS}", - "description": "Bot engine runner supervisor", + "description": "Channel engine supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -687,12 +724,12 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 8, "y": 2 }, - "id": 35, + "id": 33, "interval": null, "links": [], "mappingType": 1, @@ -729,16 +766,17 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_bot_engine_runner_sup\"}", + "expr": "min(automate_service{name=\"automate_channel_engine\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "0,0.5", - "title": "Bot engine runner", + "title": "Channel engine", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -771,6 +809,12 @@ ], "datasource": "${DS_PROMETHEUS}", "description": "REST API supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -780,10 +824,10 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 0, - "y": 3 + "y": 4 }, "id": 31, "interval": null, @@ -822,15 +866,18 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_rest_api_sup\"}", + "expr": "min(automate_service{name=\"automate_rest_api\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "0,0.5", + "timeFrom": null, + "timeShift": null, "title": "REST API", "type": "singlestat", "valueFontSize": "80%", @@ -863,7 +910,13 @@ "#299c46" ], "datasource": "${DS_PROMETHEUS}", - "description": "Channel engine monitor", + "description": "Bot engine thread runner supervisor", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "none", "gauge": { "maxValue": 100, @@ -873,12 +926,12 @@ "thresholdMarkers": false }, "gridPos": { - "h": 1, + "h": 2, "w": 4, "x": 4, - "y": 3 + "y": 4 }, - "id": 28, + "id": 58, "interval": null, "links": [], "mappingType": 1, @@ -915,22 +968,23 @@ "tableColumn": "", "targets": [ { - "expr": "automate_service{name=\"automate_channel_engine\"}", + "expr": "min(automate_service{name=\"automate_bot_engine_thread_runner\"})", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "0,0.5", - "title": "Channel engine monitor", + "title": "Bot engine thread runner", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", - "text": "N/A", + "text": "DOWN", "value": "null" }, { @@ -947,109 +1001,1171 @@ "valueName": "avg" }, { - "cacheTimeout": null, - "colorBackground": true, - "colorValue": false, - "colors": [ - "#6d1f62", - "#d44a3a", - "#299c46" - ], + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, "datasource": "${DS_PROMETHEUS}", - "description": "Bot engine runner supervisor", - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": false + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] }, + "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 1, + "h": 7, "w": 4, "x": 8, - "y": 3 + "y": 4 }, - "id": 58, - "interval": null, + "hiddenSeries": false, + "id": 82, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false + "nullPointMode": "null", + "options": { + "alertThreshold": true }, - "tableColumn": "", + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 0.5, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, "targets": [ { - "expr": "automate_service{name=\"automate_bot_engine_thread_runner_sup\"}", + "expr": "min(automate_bridges_count{visibility=\"public\",pod=\"$backnode\"})", "format": "time_series", - "instant": true, "intervalFactor": 1, - "legendFormat": "", + "legendFormat": "Public", "refId": "A" - } - ], - "thresholds": "0,0.5", - "title": "Bot engine thread runner", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "DOWN", - "value": "null" }, { - "op": "=", - "text": "DOWN", - "value": "0" + "expr": "min(automate_bridges_count{visibility=\"private\",pod=\"$backnode\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Private", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Bridge count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "custom": {}, + "mappings": [ + { + "$$hashKey": "object:441", + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "min": 0, + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 2, + "x": 12, + "y": 5 + }, + "id": 83, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.3.7", + "targets": [ + { + "expr": "min(automate_bridges_unique_connections_count{pod=\"$backnode\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Bridges connected", + "type": "stat" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 14, + "y": 5 + }, + "id": 43, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "min(automate_user_count)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Registered", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Total registered", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "custom": {}, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "min": 0, + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 2, + "x": 17, + "y": 5 + }, + "id": 73, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.3.7", + "targets": [ + { + "expr": "min(automate_logged_users_last_week)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Unique last week", + "type": "stat" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 19, + "y": 5 + }, + "id": 84, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "min(automate_group_count)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Groups", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Groups", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlBl" + }, + "custom": {}, + "decimals": 0, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "min": 0, + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 2, + "x": 22, + "y": 5 + }, + "id": 81, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.3.7", + "targets": [ + { + "expr": "min(automate_bot_count{pod=\"$backnode\",state=\"workers\"})", + "format": "time_series", + "instant": false, + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Programs", + "type": "stat" + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 0.5 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "10s", + "now" + ] + }, + "reducer": { + "params": [], + "type": "min" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0m", + "frequency": "60s", + "handler": 1, + "message": "Failing plaza service", + "name": "Plaza services alert", + "noDataState": "alerting", + "notifications": [ + { + "id": 1 + } + ] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 6 + }, + "hiddenSeries": false, + "id": 37, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 0.5, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "count(min(automate_service)) - sum(min(automate_service)) OR on() vector(8)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 0.5 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Failing services", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:205", + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": "1", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:206", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "custom": {}, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "min": 0, + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 14, + "y": 8 + }, + "id": 72, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.3.7", + "targets": [ + { + "expr": "min(automate_registered_users_last_week)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "People registered last week", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Reg. last week", + "type": "stat" + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "custom": {}, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "N/A", + "type": 1, + "value": "null" + } + ], + "min": 0, + "nullValueMode": "connected", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 19, + "y": 8 + }, + "id": 85, + "interval": null, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.3.7", + "targets": [ + { + "expr": "min(automate_created_groups_last_week)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Groups", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Created last week", + "type": "stat" + }, + { + "collapsed": false, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 79, + "panels": [], + "title": "Error generation", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 2, + "x": 0, + "y": 12 + }, + "id": 70, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(automate_program_log_count{severity=\"error\",pod=\"plaza-backend-0\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Program error log count", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Program error log count", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ { "op": "=", - "text": "UP", - "value": "1" + "text": "N/A", + "value": "null" } ], "valueName": "avg" }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 22, + "x": 2, + "y": 12 + }, + "hiddenSeries": false, + "id": 77, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (log_type) (rate(automate_program_log_count{pod=\"$backnode\",severity=\"error\"}[1h]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{log_type}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Error log generation rate by type", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": {}, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0, + "value": null + }, + { + "color": "red", + "index": 1, + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 80, + "links": [], + "options": { + "displayMode": "basic", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showUnfilled": false + }, + "pluginVersion": "7.3.7", + "targets": [ + { + "expr": "sum by (log_type) (automate_program_log_count{pod=\"$backnode\",severity=\"error\"})", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{log_type}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Error count by type", + "type": "bargauge" + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": {}, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0, + "value": null + }, + { + "color": "red", + "index": 1, + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 86, + "links": [], + "options": { + "displayMode": "basic", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showUnfilled": false + }, + "pluginVersion": "7.3.7", + "targets": [ + { + "expr": "sum by (severity) (automate_program_log_count{pod=\"$backnode\"})", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{log_type}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Logs by severity", + "type": "bargauge" + }, { "collapsed": false, + "datasource": "${DS_PROMETHEUS}", "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 5 + "y": 25 }, "id": 39, "panels": [], - "title": "Stats", + "title": "Programs", "type": "row" }, { @@ -1058,13 +2174,22 @@ "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 6, "w": 6, "x": 0, - "y": 6 + "y": 26 }, + "hiddenSeries": false, "id": 50, "legend": { "avg": false, @@ -1079,7 +2204,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 0.5, "points": true, "renderer": "flot", @@ -1151,14 +2280,23 @@ "dashes": false, "datasource": "${DS_PROMETHEUS}", "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 6, - "w": 8, + "w": 7, "x": 6, - "y": 6 + "y": 26 }, - "id": 59, + "hiddenSeries": false, + "id": 41, "legend": { "avg": false, "current": false, @@ -1173,7 +2311,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 0.5, "points": true, "renderer": "flot", @@ -1183,25 +2325,25 @@ "steppedLine": true, "targets": [ { - "expr": "automate_program_thread_count{state=\"running\"}", + "expr": "avg(automate_bot_count{state=\"running\"})", "format": "time_series", "intervalFactor": 1, - "legendFormat": "Threads ruinning", - "refId": "C" + "legendFormat": "Programs running", + "refId": "A" }, { - "expr": "automate_program_thread_count{state=\"total\"}", + "expr": "avg(automate_bot_count{state=\"total\"})", "format": "time_series", "intervalFactor": 1, - "legendFormat": "Threads total", - "refId": "D" + "legendFormat": "Programs total", + "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Running threads", + "title": "Running programs", "tooltip": { "shared": true, "sort": 0, @@ -1222,7 +2364,7 @@ "label": null, "logBase": 1, "max": null, - "min": "0", + "min": null, "show": true }, { @@ -1244,19 +2386,31 @@ "bars": false, "dashLength": 10, "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 6, - "w": 10, - "x": 14, - "y": 6 + "w": 7, + "x": 13, + "y": 26 }, - "id": 57, + "hiddenSeries": false, + "id": 59, "legend": { "avg": false, "current": false, "max": false, "min": false, + "rightSide": false, "show": true, "total": false, "values": false @@ -1265,35 +2419,39 @@ "linewidth": 1, "links": [], "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, - "pointradius": 1, + "pluginVersion": "7.3.7", + "pointradius": 0.5, "points": true, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, - "steppedLine": false, + "steppedLine": true, "targets": [ { - "expr": "sum(rate(automate_bridge_engine_messages_from_bridge[$interval]))", + "expr": "sum(automate_program_thread_count{state=\"running\"})", "format": "time_series", "intervalFactor": 1, - "legendFormat": "From bridge", - "refId": "A" + "legendFormat": "Threads ruinning", + "refId": "C" }, { - "expr": "sum(rate(automate_bridge_engine_messages_to_bridge[$interval]))", + "expr": "sum(automate_program_thread_count{state=\"total\"})", "format": "time_series", "intervalFactor": 1, - "legendFormat": "To bridge", - "refId": "B" + "legendFormat": "Threads total", + "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Messages to/from bridge", + "title": "Running threads", "tooltip": { "shared": true, "sort": 0, @@ -1309,7 +2467,8 @@ }, "yaxes": [ { - "format": "iops", + "decimals": 0, + "format": "locale", "label": null, "logBase": 1, "max": null, @@ -1331,20 +2490,86 @@ } }, { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 0.5 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "1m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + }, + { + "evaluator": { + "params": [ + 0.5 + ], + "type": "gt" + }, + "operator": { + "type": "or" + }, + "query": { + "params": [ + "B", + "1m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "2m", + "frequency": "30s", + "handler": 1, + "name": "Running programs discrepancy alert", + "noDataState": "no_data", + "notifications": [] + }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 5, - "w": 6, - "x": 0, - "y": 12 + "h": 6, + "w": 4, + "x": 9, + "y": 32 }, - "id": 41, + "hiddenSeries": false, + "id": 74, "legend": { "avg": false, "current": false, @@ -1359,7 +2584,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 0.5, "points": true, "renderer": "flot", @@ -1369,25 +2598,33 @@ "steppedLine": true, "targets": [ { - "expr": "automate_bot_count{state=\"running\"}", + "expr": "max(automate_bot_count{state=\"running\"}) - min(automate_bot_count{state=\"running\"})", "format": "time_series", "intervalFactor": 1, "legendFormat": "Programs running", "refId": "A" }, { - "expr": "automate_bot_count{state=\"total\"}", + "expr": "max(automate_bot_count{state=\"total\"}) - min(automate_bot_count{state=\"total\"})", "format": "time_series", "intervalFactor": 1, "legendFormat": "Programs total", "refId": "B" } ], - "thresholds": [], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 0.5 + } + ], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Running programs", + "title": "Running programs discrepancy", "tooltip": { "shared": true, "sort": 0, @@ -1426,26 +2663,89 @@ } }, { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 0.5 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "C", + "1m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + }, + { + "evaluator": { + "params": [ + 0.5 + ], + "type": "gt" + }, + "operator": { + "type": "or" + }, + "query": { + "params": [ + "D", + "1m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "2m", + "frequency": "15s", + "handler": 1, + "name": "Running threads discrepancy alert", + "noDataState": "no_data", + "notifications": [] + }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 5, - "w": 8, - "x": 6, - "y": 12 + "h": 6, + "w": 7, + "x": 13, + "y": 32 }, - "id": 44, + "hiddenSeries": false, + "id": 75, "legend": { - "alignAsTable": false, "avg": false, "current": false, - "hideEmpty": false, - "hideZero": true, "max": false, "min": false, "rightSide": false, @@ -1456,8 +2756,12 @@ "lines": true, "linewidth": 1, "links": [], - "nullPointMode": "null", + "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 0.5, "points": true, "renderer": "flot", @@ -1467,26 +2771,33 @@ "steppedLine": true, "targets": [ { - "expr": "automate_service_count{visibility=\"all\"}", + "expr": "max(automate_program_thread_count{state=\"running\"}) - min(automate_program_thread_count{state=\"running\"})", "format": "time_series", - "instant": false, "intervalFactor": 1, - "legendFormat": "All", - "refId": "B" + "legendFormat": "Threads ruinning", + "refId": "C" }, { - "expr": "automate_service_count{visibility=\"public\"}", + "expr": "max(automate_program_thread_count{state=\"total\"}) - min(automate_program_thread_count{state=\"total\"})", "format": "time_series", "intervalFactor": 1, - "legendFormat": "Public", - "refId": "A" + "legendFormat": "Threads total", + "refId": "D" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 0.5 } ], - "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Services", + "title": "Running threads discrepancy", "tooltip": { "shared": true, "sort": 0, @@ -1503,47 +2814,247 @@ "yaxes": [ { "decimals": 0, + "format": "locale", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { "format": "short", - "label": "", + "label": null, "logBase": 1, "max": null, "min": null, "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 65, + "panels": [], + "title": "Bridge data", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 0, + "y": 39 + }, + "id": 44, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "pluginVersion": "6.1.3", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg(automate_bridges_count{visibility=\"public\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Public bridges", + "refId": "C" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Public Bridges", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 3, + "y": 39 + }, + "id": 66, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "pluginVersion": "6.1.3", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg(automate_bridges_count{visibility=\"private\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Public bridges", + "refId": "C" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Private Bridges", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" } ], - "yaxis": { - "align": false, - "alignLevel": null - } + "valueName": "current" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "description": "Messages getting in and out of the channel and bridge engines", + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 5, - "w": 10, - "x": 14, - "y": 12 + "h": 6, + "w": 7, + "x": 6, + "y": 39 }, - "id": 53, + "hiddenSeries": false, + "id": 57, "legend": { - "alignAsTable": false, "avg": false, "current": false, - "hideEmpty": false, - "hideZero": false, "max": false, "min": false, "show": true, @@ -1554,7 +3065,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 1, "points": true, "renderer": "flot", @@ -1564,18 +3079,17 @@ "steppedLine": false, "targets": [ { - "expr": "sum(rate(automate_channel_engine_messages_in[$interval]))", + "expr": "sum(rate(automate_bridge_engine_messages_from_bridge[$interval]))", "format": "time_series", - "instant": false, "intervalFactor": 1, - "legendFormat": "Channel engine Fan IN", + "legendFormat": "From bridge", "refId": "A" }, { - "expr": "sum(rate(automate_channel_engine_messages_out[$interval]))", + "expr": "sum(rate(automate_bridge_engine_messages_to_bridge[$interval]))", "format": "time_series", "intervalFactor": 1, - "legendFormat": "Channel engine Fan OUT", + "legendFormat": "To bridge", "refId": "B" } ], @@ -1583,7 +3097,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Messages passed", + "title": "Messages to/from bridge", "tooltip": { "shared": true, "sort": 0, @@ -1603,7 +3117,7 @@ "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { @@ -1620,28 +3134,52 @@ "alignLevel": null } }, + { + "collapsed": false, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 45 + }, + "id": 68, + "panels": [], + "title": "Internals", + "type": "row" + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", - "decimals": null, - "description": "Creation rate per minute", + "description": "Messages getting in and out of the channel and bridge engines", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 5, - "w": 8, + "h": 6, + "w": 6, "x": 0, - "y": 17 + "y": 46 }, - "id": 46, + "hiddenSeries": false, + "id": 53, "legend": { + "alignAsTable": false, "avg": false, "current": false, + "hideEmpty": false, + "hideZero": false, "max": false, "min": false, - "rightSide": false, "show": true, "total": false, "values": false @@ -1649,9 +3187,13 @@ "lines": true, "linewidth": 1, "links": [], - "nullPointMode": "null", + "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, - "pointradius": 0.5, + "pluginVersion": "7.3.7", + "pointradius": 1, "points": true, "renderer": "flot", "seriesOverrides": [], @@ -1660,39 +3202,26 @@ "steppedLine": false, "targets": [ { - "expr": "rate(automate_bot_count{state=\"total\"}[$interval])", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Bots", - "refId": "B" - }, - { - "expr": "rate(automate_monitor_count{state=\"total\"}[$interval])", + "expr": "sum(rate(automate_channel_engine_messages_in[$interval]))", "format": "time_series", + "instant": false, "intervalFactor": 1, - "legendFormat": "Monitors", + "legendFormat": "Channel engine Fan IN", "refId": "A" }, { - "expr": "rate(automate_service_count{visibility=\"all\"}[$interval])", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Services", - "refId": "C" - }, - { - "expr": "rate(automate_user_count{state=\"registered\"}[$interval])", + "expr": "sum(rate(automate_channel_engine_messages_out[$interval]))", "format": "time_series", "intervalFactor": 1, - "legendFormat": "Users", - "refId": "D" + "legendFormat": "Channel engine Fan OUT", + "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Evolution #/$interval", + "title": "Messages passed in channels", "tooltip": { "shared": true, "sort": 0, @@ -1708,7 +3237,7 @@ }, "yaxes": [ { - "format": "short", + "format": "iops", "label": null, "logBase": 1, "max": null, @@ -1729,20 +3258,43 @@ "alignLevel": null } }, + { + "collapsed": false, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 61, + "panels": [], + "title": "Monitors & Misc", + "type": "row" + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 5, - "w": 6, - "x": 8, - "y": 17 + "h": 6, + "w": 8, + "x": 0, + "y": 53 }, - "id": 43, + "hiddenSeries": false, + "id": 48, "legend": { "avg": false, "current": false, @@ -1755,21 +3307,26 @@ "lines": true, "linewidth": 1, "links": [], - "nullPointMode": "null", + "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 0.5, "points": true, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, - "steppedLine": true, + "steppedLine": false, "targets": [ { - "expr": "automate_user_count", + "expr": "sum(rate(automate_monitor_trigger[$interval]))", "format": "time_series", + "interval": "", "intervalFactor": 1, - "legendFormat": "Registered", + "legendFormat": "Triggers", "refId": "A" } ], @@ -1777,7 +3334,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Registered users", + "title": "Monitor usage", "tooltip": { "shared": true, "sort": 0, @@ -1793,12 +3350,12 @@ }, "yaxes": [ { - "decimals": 0, - "format": "short", - "label": null, + "decimals": null, + "format": "ops", + "label": "", "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { @@ -1821,19 +3378,30 @@ "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", + "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 6, - "w": 8, - "x": 0, - "y": 22 + "w": 6, + "x": 8, + "y": 53 }, - "id": 48, + "hiddenSeries": false, + "id": 45, "legend": { "avg": false, "current": false, "max": false, "min": false, + "rightSide": false, "show": true, "total": false, "values": false @@ -1841,22 +3409,33 @@ "lines": true, "linewidth": 1, "links": [], - "nullPointMode": "null as zero", + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 0.5, "points": true, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, - "steppedLine": false, + "steppedLine": true, "targets": [ { - "expr": "sum(rate(automate_monitor_trigger[$interval]))", + "expr": "min(automate_monitor_count{state=\"total\"})", "format": "time_series", - "interval": "", + "hide": false, "intervalFactor": 1, - "legendFormat": "Triggers", + "legendFormat": "Total", + "refId": "B" + }, + { + "expr": "min(automate_monitor_count{state=\"running\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Running", "refId": "A" } ], @@ -1864,7 +3443,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Monitor usage", + "title": "Running monitors", "tooltip": { "shared": true, "sort": 0, @@ -1880,12 +3459,12 @@ }, "yaxes": [ { - "decimals": null, - "format": "ops", - "label": "", + "decimals": 0, + "format": "short", + "label": null, "logBase": 1, "max": null, - "min": "0", + "min": null, "show": true }, { @@ -1909,14 +3488,24 @@ "dashes": false, "datasource": "${DS_PROMETHEUS}", "decimals": null, + "description": "Creation rate per minute", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { - "h": 6, - "w": 6, - "x": 8, - "y": 22 + "h": 5, + "w": 7, + "x": 14, + "y": 53 }, - "id": 45, + "hiddenSeries": false, + "id": 46, "legend": { "avg": false, "current": false, @@ -1931,36 +3520,53 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 0.5, "points": true, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, - "steppedLine": true, + "steppedLine": false, "targets": [ { - "expr": "automate_monitor_count{state=\"total\"}", + "expr": "avg(rate(automate_bot_count{state=\"total\"}[$interval]))", "format": "time_series", - "hide": false, "intervalFactor": 1, - "legendFormat": "Total", + "legendFormat": "Bots", "refId": "B" }, { - "expr": "automate_monitor_count{state=\"running\"}", + "expr": "avg(rate(automate_monitor_count{state=\"total\"}[$interval]))", "format": "time_series", "intervalFactor": 1, - "legendFormat": "Running", + "legendFormat": "Monitors", "refId": "A" + }, + { + "expr": "avg(rate(automate_service_count{visibility=\"all\"}[$interval]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Services", + "refId": "C" + }, + { + "expr": "avg(rate(automate_user_count{state=\"registered\"}[$interval]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Users", + "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Running monitors", + "title": "Evolution #/$interval", "tooltip": { "shared": true, "sort": 0, @@ -1976,7 +3582,6 @@ }, "yaxes": [ { - "decimals": 0, "format": "short", "label": null, "logBase": 1, @@ -2000,11 +3605,12 @@ }, { "collapsed": false, + "datasource": "${DS_PROMETHEUS}", "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 28 + "y": 59 }, "id": 20, "panels": [], @@ -2022,6 +3628,12 @@ ], "datasource": "${DS_PROMETHEUS}", "decimals": 1, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "format": "dtdurations", "gauge": { "maxValue": 100, @@ -2034,7 +3646,7 @@ "h": 7, "w": 6, "x": 0, - "y": 29 + "y": 60 }, "id": 16, "interval": null, @@ -2102,14 +3714,23 @@ "datasource": "${DS_PROMETHEUS}", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 6, - "y": 29 + "y": 60 }, + "hiddenSeries": false, "id": 11, "legend": { "avg": false, @@ -2124,7 +3745,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2134,7 +3759,7 @@ "steppedLine": false, "targets": [ { - "expr": "process_virtual_memory_bytes{instance=\"172.17.0.1:9212\"}", + "expr": "process_virtual_memory_bytes{instance=\"$node\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "Virtual memory", @@ -2142,7 +3767,7 @@ "step": 2 }, { - "expr": "process_resident_memory_bytes{instance=\"172.17.0.1:9212\"}", + "expr": "process_resident_memory_bytes{instance=\"$node\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "Resident Memory", @@ -2201,14 +3826,23 @@ "decimals": null, "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 14, - "y": 29 + "y": 60 }, + "hiddenSeries": false, "id": 10, "legend": { "avg": false, @@ -2223,7 +3857,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2238,7 +3876,7 @@ "steppedLine": false, "targets": [ { - "expr": "process_threads_total{instance=\"172.17.0.1:9212\"}", + "expr": "process_threads_total{instance=\"$node\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "Threads", @@ -2247,7 +3885,7 @@ "step": 2 }, { - "expr": "sum(irate(process_cpu_seconds_total{instance=\"172.17.0.1:9212\"}[$interval])) without (kind) * 100", + "expr": "sum(irate(process_cpu_seconds_total{instance=\"$node\"}[$interval])) without (kind) * 100", "format": "time_series", "intervalFactor": 2, "legendFormat": "CPU", @@ -2299,11 +3937,12 @@ }, { "collapsed": false, + "datasource": "${DS_PROMETHEUS}", "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 36 + "y": 67 }, "id": 18, "panels": [], @@ -2318,14 +3957,23 @@ "datasource": "${DS_PROMETHEUS}", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 0, - "y": 37 + "y": 68 }, + "hiddenSeries": false, "id": 6, "legend": { "avg": false, @@ -2340,7 +3988,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2414,13 +4066,22 @@ "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, - "y": 37 + "y": 68 }, + "hiddenSeries": false, "id": 14, "legend": { "avg": false, @@ -2435,7 +4096,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2519,14 +4184,23 @@ "datasource": "${DS_PROMETHEUS}", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, - "y": 37 + "y": 68 }, + "hiddenSeries": false, "id": 3, "legend": { "avg": false, @@ -2541,7 +4215,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2637,13 +4315,22 @@ "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, - "y": 44 + "y": 75 }, + "hiddenSeries": false, "id": 15, "legend": { "avg": false, @@ -2658,7 +4345,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2746,14 +4437,23 @@ "datasource": "${DS_PROMETHEUS}", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 8, - "y": 44 + "y": 75 }, + "hiddenSeries": false, "id": 7, "legend": { "alignAsTable": false, @@ -2770,7 +4470,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2855,14 +4559,23 @@ "datasource": "${DS_PROMETHEUS}", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, - "y": 44 + "y": 75 }, + "hiddenSeries": false, "id": 8, "legend": { "avg": false, @@ -2877,7 +4590,11 @@ "linewidth": 2, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2979,13 +4696,22 @@ "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, - "y": 51 + "y": 82 }, + "hiddenSeries": false, "id": 12, "legend": { "avg": false, @@ -3000,7 +4726,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -3081,8 +4811,8 @@ } } ], - "refresh": "5s", - "schemaVersion": 18, + "refresh": "10s", + "schemaVersion": 26, "style": "dark", "tags": [], "templating": { @@ -3091,7 +4821,8 @@ "allValue": null, "current": {}, "datasource": "${DS_PROMETHEUS}", - "definition": "", + "definition": "label_values(erlang_vm_process_count, instance)", + "error": null, "hide": 0, "includeAll": false, "label": "Node", @@ -3105,8 +4836,10 @@ "sort": 1, "tagValuesQuery": "label_values({job=\"$tag\"},instance)", "tags": [ - "blackbox", - "plaza-dev" + { + "selected": false, + "text": "plaza-backend" + } ], "tagsQuery": "label_values(job)", "type": "query", @@ -3117,9 +4850,11 @@ "auto_count": 30, "auto_min": "10s", "current": { + "selected": false, "text": "1m", "value": "1m" }, + "error": null, "hide": 0, "label": null, "name": "interval", @@ -3184,11 +4919,34 @@ "refresh": 2, "skipUrlSync": false, "type": "interval" + }, + { + "allValue": null, + "current": {}, + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(erlang_vm_process_count,pod) ", + "error": null, + "hide": 0, + "includeAll": false, + "label": "Backend-node", + "multi": false, + "name": "backnode", + "options": [], + "query": "label_values(erlang_vm_process_count,pod) ", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false } ] }, "time": { - "from": "now-30m", + "from": "now-3h", "to": "now" }, "timepicker": { @@ -3218,7 +4976,7 @@ ] }, "timezone": "browser", - "title": "Plaza", - "uid": "iKcIpW8mk", - "version": 11 -} + "title": "PrograMaker", + "uid": "iKcIpW8mk-2", + "version": 12 +} \ No newline at end of file diff --git a/utils/monitoring/prometheus/prometheus.yml b/utils/monitoring/prometheus/prometheus.yml index ece1dba4..569deb17 100644 --- a/utils/monitoring/prometheus/prometheus.yml +++ b/utils/monitoring/prometheus/prometheus.yml @@ -32,7 +32,7 @@ scrape_configs: # - fluentd:24231 # - 172.17.0.1:9211 - - job_name: 'plaza-dev' + - job_name: 'programaker-dev' scheme: http static_configs: - targets: diff --git a/utils/multinode-tests/docker-compose-3-nodes.yml b/utils/multinode-tests/docker-compose-3-nodes.yml index 656b7820..4548db07 100644 --- a/utils/multinode-tests/docker-compose-3-nodes.yml +++ b/utils/multinode-tests/docker-compose-3-nodes.yml @@ -2,71 +2,71 @@ version: '3' services: - plaza-router: + programaker-router: build: ../router ports: - 8080:80 links: - - plaza-frontend - - plaza-backend-0:plaza-backend + - programaker-frontend + - programaker-backend-0: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-0: + programaker-backend-0: build: ../../backend - hostname: plaza-backend-0 + hostname: programaker-backend-0 ports: - 8888:8888 volumes: - - plaza-dev-backend-mnesia-0:/app/mnesia + - programaker-dev-backend-mnesia-0:/app/mnesia environment: - "NODE_NAME_SUFFIX=" - "NODE_WITH_HOSTNAME=1" - - "AUTOMATE_SYNC_PRIMARY=backend@plaza-backend-0" - - "AUTOMATE_SYNC_PEERS=backend@plaza-backend-0,backend@plaza-backend-1,backend@plaza-backend-2" + - "AUTOMATE_SYNC_PRIMARY=backend@programaker-backend-0" + - "AUTOMATE_SYNC_PEERS=backend@programaker-backend-0,backend@programaker-backend-1,backend@programaker-backend-2" - "MNESIA_DIR=/app/mnesia" - plaza-backend-1: + programaker-backend-1: build: ../../backend - hostname: plaza-backend-1 + hostname: programaker-backend-1 ports: - 8889:8888 volumes: - - plaza-dev-backend-mnesia-1:/app/mnesia + - programaker-dev-backend-mnesia-1:/app/mnesia environment: - "NODE_NAME_SUFFIX=" - "NODE_WITH_HOSTNAME=1" - - "AUTOMATE_SYNC_PRIMARY=backend@plaza-backend-0" - - "AUTOMATE_SYNC_PEERS=backend@plaza-backend-0,backend@plaza-backend-1,backend@plaza-backend-2" + - "AUTOMATE_SYNC_PRIMARY=backend@programaker-backend-0" + - "AUTOMATE_SYNC_PEERS=backend@programaker-backend-0,backend@programaker-backend-1,backend@programaker-backend-2" - "MNESIA_DIR=/app/mnesia" - plaza-backend-2: + programaker-backend-2: build: ../../backend - hostname: plaza-backend-2 + hostname: programaker-backend-2 ports: - 8890:8888 volumes: - - plaza-dev-backend-mnesia-2:/app/mnesia + - programaker-dev-backend-mnesia-2:/app/mnesia environment: - "NODE_NAME_SUFFIX=" - "NODE_WITH_HOSTNAME=1" - - "AUTOMATE_SYNC_PRIMARY=backend@plaza-backend-0" - - "AUTOMATE_SYNC_PEERS=backend@plaza-backend-0,backend@plaza-backend-1,backend@plaza-backend-2" + - "AUTOMATE_SYNC_PRIMARY=backend@programaker-backend-0" + - "AUTOMATE_SYNC_PEERS=backend@programaker-backend-0,backend@programaker-backend-1,backend@programaker-backend-2" - "MNESIA_DIR=/app/mnesia" volumes: - plaza-dev-backend-mnesia-0: - plaza-dev-backend-mnesia-1: - plaza-dev-backend-mnesia-2: + programaker-dev-backend-mnesia-0: + programaker-dev-backend-mnesia-1: + programaker-dev-backend-mnesia-2: networks: default: external: - name: plaza-dev-3-nodes + name: programaker-dev-3-nodes diff --git a/utils/test.sh b/utils/test.sh index 1ddf592f..8247b006 100755 --- a/utils/test.sh +++ b/utils/test.sh @@ -28,5 +28,5 @@ pushd addons npm install . npm run build npm run lint -make dist/plaza.xpi +make dist/programaker.xpi popd diff --git a/utils/testing/.gitignore b/utils/testing/.gitignore new file mode 100644 index 00000000..9c06f38e --- /dev/null +++ b/utils/testing/.gitignore @@ -0,0 +1,8 @@ +plan.svg +plan.gv +results.svg + +api_test_logs.txt + +.mypy_cache +__pycache__ diff --git a/utils/testing/README.md b/utils/testing/README.md new file mode 100644 index 00000000..85cfb074 --- /dev/null +++ b/utils/testing/README.md @@ -0,0 +1,21 @@ +# Automatic API testing + +Please note that this is a Work In Progress. +It's currently being used to perform some assertions over the permissions that the API takes in to account, and so it's useful to have it along the code. **But no refactoring has been done to this scripts after the first experiments with them, so don't read them expecting cleanliness.** + +## Usage + +There are two main scripts in this folder, `gen_test_api_plan.py` and `run_plan.py`. + + - `gen_test_api_plan.py` takes an API permission matrix (the one on [docs/test-table.csv](../../docs/test-table.csv)) and generates a [Graphviz] graph describing a plan that can be "executed" to check that a given API server complies with the permissions described on the file. By default the plan is written to the `plan.gv` file, and can be visualized by opening the `plan.svg` file with a browser or a SVG viewer. + - `run_plan.py` takes the `plan.gv` file created by the previous script and runs it over a given API server. It will generate a `results.svg` file as a visualization of the tests performed (red boxes indicate failed tests, blue ones indicate ignored and green ones indicate passed ones). It takes into account the `API_TEST_ROOT` and `API_TEST_DOCKER` environment variables. + - `API_TEST_ROOT`: Points to the API root of the tested API server (default: `http://localhost:8881/api/v0`). + - `API_TEST_DOCKER`: Refers to the name of the Docker container where the tests are to be performed (`back-test-docker`). + +Note that the execution of the `run_plan.py` script will make a significant number of API calls in a short amount of time (>750 in ~10 seconds at the time of writting this), will require Docker-exec access to the server, and won't cleanup the resources created in the process, so it's preferable to not use a shared server as target.** + +Consider using the `run-api-test.sh` shell script to launch a local Docker container and run there the tests. + +Access to the Docker container is needed to promote users to admins, which cannnot be done through the API. + +[Graphviz]: https://graphviz.org/ diff --git a/utils/testing/builder.py b/utils/testing/builder.py new file mode 100644 index 00000000..6bbb6285 --- /dev/null +++ b/utils/testing/builder.py @@ -0,0 +1,149 @@ +import os +import re +import uuid + + +def gen_rand(): + return str(uuid.uuid4()).replace('-', '') + + +_up = os.path.dirname + +SAMPLE_PICTURE = os.path.join(_up(_up(_up(os.path.abspath(__file__)))), + 'frontend', 'src', 'assets', 'about-logo.png') + + +def build_data_for_query(verb, endpoint, ctx): + if verb.lower() == 'get': + if endpoint == '/utils/autocomplete/users': + return {'q': ctx.get('user_name', 'sample')} + else: + return {} + + if endpoint == '/sessions/register': + id = 'test' + gen_rand() + passwd = 'pass' + gen_rand() + ctx['user_name'] = id + ctx['password'] = passwd + + return { + 'email': id + '@test' + id + '.com', + 'password': passwd, + 'username': id, + } + + elif endpoint == '/sessions/login': + return { + 'password': ctx['password'], + 'username': ctx['user_name'], + } + + elif endpoint == '/users/:user_name/monitors' or endpoint == '/programs/by-id/:program_id/monitors': + id = gen_rand() + return { + "type": "http", + "value": id, + "name": id, + } + + elif endpoint in ('/users/id/:user_id/programs/id/:program_id/tags', + '/programs/by-id/:program_id/tags'): + tags = [gen_rand() for _ in range(4)] + ctx['program_tags'] = tags + return { + 'tags': tags, + } + + elif endpoint == '/groups': + group_name = 'group' + gen_rand() + ctx['group_name'] = group_name + return { + "name": group_name, + "public": False, + } + + elif endpoint in ('/users/:user_name/bridges', + '/users/id/:user_id/bridges'): + bridge_name = 'bridge' + gen_rand() + ctx['bridge_name'] = bridge_name + return { + "name": bridge_name, + "public": False, + } + + elif endpoint == '/users/id/:user_id/templates': + template_name = 'template' + gen_rand() + ctx['template_name'] = template_name + content = 'this is a template test' + return { + "name": template_name, + "content": content, + } + + elif endpoint == '/users/id/:user_id/custom_signals': + signal_name = 'signal' + gen_rand() + ctx['signal_name'] = signal_name + return { + "name": signal_name, + } + + elif endpoint == '/groups/by-id/:group_id/picture': + return ({}, { + 'file': open(SAMPLE_PICTURE, 'rb'), + }) + + elif endpoint in ('/users/id/:user_id/programs/id/:program_id/status', + '/programs/by-id/:program_id/status'): + return { + 'enable': True, + } + + elif endpoint == '/groups/by-id/:group_id/bridges': + bridge_name = 'bridge' + gen_rand() + ctx['bridge_name'] = bridge_name + return { + "name": bridge_name, + } + + else: + return {} + + +def update_ctx(verb, endpoint, res, ctx): + if verb == 'post' and endpoint == '/sessions/login': + data = res.json() + ctx['token'] = data['token'] + ctx['user_id'] = data['user_id'] + + elif verb == 'post' and endpoint == '/groups': + data = res.json() + ctx['group_id'] = data['group']['id'] + + elif verb == 'post' and endpoint == '/groups/by-id/:group_id/programs': + data = res.json() + ctx['program_id'] = data['id'] + + elif verb == 'post' and endpoint == '/users/:user_name/programs': + data = res.json() + ctx['program_id'] = data['id'] + + elif verb == 'post' and endpoint in ('/users/:user_name/bridges', + '/users/id/:user_id/bridges'): + data = res.json() + bridge_id = data['control_url'].rstrip('/').split('/')[-2] + ctx['bridge_id'] = bridge_id + + elif verb == 'post' and endpoint == '/users/id/:user_id/templates': + data = res.json() + ctx['template_id'] = data['id'] + + elif verb == 'post' and endpoint == '/users/id/:user_id/custom_signals': + data = res.json() + ctx['signal_id'] = data['id'] + + +def fill_path_params(endpoint, ctx): + def _fill_param(chunk): + return ctx[chunk.group(1)] + + return re.sub(r':([a-zA-Z_]+)', _fill_param, endpoint) diff --git a/utils/testing/gen_test_api_plan.py b/utils/testing/gen_test_api_plan.py new file mode 100644 index 00000000..92988266 --- /dev/null +++ b/utils/testing/gen_test_api_plan.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python3 + +import csv +import itertools +import json +import logging +import os +import re +import uuid + +import pygraphviz as pgv + +DATA_FILE = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__)))), 'docs', 'test-table.csv') + +GET_URL_PARAM_RE = re.compile(':([a-zA-Z_]+)') + +if not os.path.exists(DATA_FILE): + raise Exception("Test table not found (expected at {})".format(DATA_FILE)) + + +def gen_id(): + return str(uuid.uuid4()) + + +def read_permission_matrix(fname): + return csv.reader(open(DATA_FILE, 'rt')) + + +def parse(rows): + groups = {} + current_group = None + sections = [] + for row in rows: + if len(row) == 0 or len(row[0]) == 0: + continue + + if row[0].startswith('/'): + if current_group is None: + raise Exception("No group defined before " + row[0]) + + if '[skip]' in row[0].lower(): + continue + + req = '' + prod = '' + if len(row) > 1: + req = row[1] + if len(row) > 2: + prod = row[2] + + element = { + 'raw': + row, + 'url': + row[0], + 'requires': [ + part.lower().strip(', ') for part in req.split(' ') + if part != '' + ], + 'produces': [ + part.lower().strip(', ') for part in prod.split(' ') + if part != '' + ], + 'sections': {}, + 'id': + gen_id(), + } + + for off, section in enumerate(sections): + if len(section) == 0: + continue + idx = off + 3 + if len(row) > idx: + element['sections'][section.upper()] = [ + part.strip(', ') for part in row[idx].split(' ') + if part != '' + ] + else: + element['sections'][section.upper()] = [] + + groups[current_group]['items'].append(element) + else: + if current_group: # Remove group if no elements were added + if len(groups[current_group]['items']) == 0: + del groups[current_group] + + current_group = row[0].lower() + groups[current_group] = {'sections': row[3:], 'items': []} + sections = row[3:] + return groups + + +def add_api_requirements(groups): + for (group, contents) in groups.items(): + for endpoint in contents['items']: + url = endpoint['url'] + requirements = [] + params = GET_URL_PARAM_RE.findall(url) + for param in params: + field = None + if '_' in param: + name, field = param.split('_') + else: + name = param + + requirements.append((name, field)) + endpoint['requires'] = merge_requirements( + requirements, [(req, None) for req in endpoint['requires']]) + + +def merge_requirements(g1, g2): + requirements = {} + for (req, prop) in g1: + if req not in requirements: + requirements[req] = prop + elif requirements[req] == prop: + requirements[req] = None + for (req, prop) in g2: + if req not in requirements: + requirements[req] = prop + elif requirements[req] == prop: + requirements[req] = None + return list(requirements.items()) + + +def complement_permissions(perms): + verbs = ('GET', 'POST', 'PUT', 'PATCH', 'DELETE') + if any([verb in perms for verb in verbs]): + # If any of the known permissions is give, negate the others + for verb in ('GET', 'POST', 'PUT', 'PATCH', 'DELETE'): + if verb not in perms: + perms.append("FAIL-" + verb) + + +def build_test_plan(groups): + plan = pgv.AGraph(directed=True, + comment='PrograMaker API test dependencies') + plan.graph_attr['rankdir'] = 'LR' + plan.node_attr['shape'] = 'rect' + + start_id = gen_id() + plan.add_node('start', label='start') + plan.add_node('logout', label='logout') + + plan.add_node('register-admin', label='POST /sessions/register') + plan.add_node('login-admin', label='POST /sessions/login') + plan.add_node('promote_to_admin', label='promote_to_admin') + + plan.add_edge('start', 'register-admin') + plan.add_edge('register-admin', 'login-admin') + plan.add_edge('login-admin', 'promote_to_admin') + + plan.add_node('register-editor', label='POST /sessions/register') + plan.add_node('make-editor', label='make-editor') + + plan.add_edge('register-editor', 'make-editor') + + plan.add_node('register-viewer', label='POST /sessions/register') + plan.add_node('make-viewer', label='make-viewer') + + plan.add_edge('register-viewer', 'make-viewer') + + has_known_productors = set() + productor_dependencies = {} + known_requirements = set() + + expanded_blocks = [] + for (group, contents) in groups.items(): + for endpoint in contents['items']: + for (section, permissions) in endpoint['sections'].items(): + + if permissions == ['IGNORE']: + # This is used for login endpoints that can be accessed + # by non-anonymous users, so not all the following combinations are explored: + # + # /session/login -> ENDPOINT_X + # /session/login -> /session/login -> ENDPOINT_X + # + # This also is used to mark ADMIN operations that can be + # done by registered user. + continue + + req = endpoint['requires'].copy() + for (dep, prop) in req: + known_requirements.add(dep) + + ep_id = gen_id() + tags = {'section': section.upper()} + produces = endpoint['produces'].copy() + if endpoint['url'] != '/sessions/login': + if section.upper() != 'ANONYMOUS' and ( + permissions == [] + and endpoint['sections']['ANONYMOUS'] != []): + continue + + # if section.upper() == 'ANONYMOUS' or ( + # permissions != endpoint['sections']['ANONYMOUS']): + + if section.upper() == 'ADMIN': + req.append(('promote_to_admin', None)) + + elif section.upper() == 'G. ADMIN': + req.append(('group', None)) + + elif section.upper() == 'G. EDITOR': + req.append(('group-editor', None)) + + elif section.upper() == 'G. VIEWER': + req.append(('group-viewer', None)) + + elif section.upper() == 'USER': + req.append(('login', None)) + + elif section.upper() == 'ANONYMOUS': + if len([(res, prop) for (res, prop) in req]) > 0: + req.append(('logout', None)) + + else: + raise Exception( + "Unknown user type: '{}' on '{}'".format( + section, endpoint['url'])) + continue + + for prod in endpoint['produces']: + has_known_productors.add(prod) + if prod not in productor_dependencies: + productor_dependencies[prod] = set() + + for (dep, prop) in req: + productor_dependencies[prod].add(dep) + + if len(permissions) == 0: + permissions = ['CHECK_NOAUTH'] + else: + complement_permissions(permissions) + + for permission in permissions: + permission_produces = produces + + if permission != 'POST': + permission_produces = [] + block = { + 'id': ep_id, + 'requires': req, + 'url': permission + ' ' + endpoint['url'], + 'produces': permission_produces, + 'tags': tags, + } + + expanded_blocks.append(block) + + sealed = set() + productors = { + 'promote_to_admin': ['promote_to_admin'], + 'logout': ['logout'], + 'group-editor': ['make-editor'], + 'group-viewer': ['make-viewer'], + } + has_known_productors.add('promote_to_admin') + productor_dependencies['promote_to_admin'] = set(['login']) + has_known_productors.add('logout') + productor_dependencies['logout'] = set(['login']) + + has_known_productors.add('group-editor') + productor_dependencies['group-editor'] = set(['group']) + has_known_productors.add('group-viewer') + productor_dependencies['group-viewer'] = set(['group']) + + for dep in known_requirements: + if dep not in has_known_productors: + logging.warning("No productor known for: {}".format(dep)) + + def is_transitively_produced(produced, requirement): + if requirement not in has_known_productors: + return False + if requirement == produced: + return True + for req in productor_dependencies[requirement]: + if is_transitively_produced(produced, req): + return True + return False + + def check_valid_order(l): + for i in range(0, len(l)): + for j in range(i + 1, len(l)): + if is_transitively_produced(l[j], l[i]): + print("Expected '{}' before '{}'".format(l[j], l[i])) + assert not is_transitively_produced(l[j], l[i]) + + def priority_sort(items): + produced_items = [] + remaining_items = items.copy() + rounds_skipped = 0 + while remaining_items: + item = remaining_items.pop( + 0 + ) # Note that we're adding items on one end and removing it on the other + if all([ + dep in produced_items + for dep in productor_dependencies[item] + ]): + produced_items.append(item) + rounds_skipped = 0 + else: + remaining_items.append(item) + rounds_skipped += 1 + + if rounds_skipped > len(remaining_items): + print( + json.dumps( + { + k: list(v) + for k, v in productor_dependencies.items() + }, + indent=4)) + raise Exception( + 'Error finding productor order. Ordered: {}. Remaining: {}.' + .format(produced_items, remaining_items)) + return produced_items + + priority_list = priority_sort(list(has_known_productors)) + + def priority_sort_blocks(blocks): + remaining_blocks = blocks.copy() + allowed_dependencies = set() + for p in [None] + priority_list: + if p is not None: + allowed_dependencies.add(p) + + skipped_blocks = [] + for block in remaining_blocks: + if all([ + dep in allowed_dependencies + for (dep, prop) in block['requires'] + ]): + yield block + else: + skipped_blocks.append(block) + remaining_blocks = skipped_blocks + + if len(remaining_blocks) > 0: + logging.warning('{} blocks left outside of priority list'.format( + len(remaining_blocks))) + for block in remaining_blocks: + yield block + + order = list(priority_sort_blocks(expanded_blocks)) + for block_num, block in enumerate(order): + requirement_combos = [] + if len(block['requires']) == 0: + requirement_combos.append(('start', )) + + doable = True + for (dep, prop) in block['requires']: + prods = productors.get(dep, []) + sealed.add(dep) + if len(prods) == 0: + if dep not in has_known_productors: + doable = False + # logging.warning("No productors for '{}' on '{}'".format( + # dep, block['url'])) + else: + raise Exception( + "No productor found for '{}' on '{}'. But productors are known. In block {}/{}" + .format(dep, block['url'], block_num, len(order))) + elif len(prods) == 1: + requirement_combos.append((prods[0], )) + else: + requirement_combos.append([prod for prod in prods]) + + combos = list(itertools.product(*requirement_combos)) + if not doable: + combos = [ + () + ] # Show the block on the graph, but outside the dependency tree + + for prod_combo in combos or [()]: + block_id = gen_id() + if block['url'] == 'POST /sessions/login': + login_id = block_id + + if block['url'] == 'POST /groups': + group_id = block_id + + for prod in block['produces']: + if prod in sealed: + raise Exception( + "'{}' produced *after* a block that required it has been placed. In block {}/{}." + .format(prod, block_num, len(order))) + + res = prod.lower().strip() + if res not in productors: + productors[res] = [] + productors[res].append(block_id) + + plan.add_node( + block_id, + label=block['url'] + + '\n\n {}'.format(json.dumps(block['tags'])), + ) + for source in prod_combo: + plan.add_edge(source, block_id) + + plan.add_edge(login_id, 'logout') + plan.add_edge(group_id, 'register-editor') + plan.add_edge(group_id, 'register-viewer') + return plan + + +def build_rev_index(plan): + edge_rev_index = {} + for (_from, to) in plan.edges(): + if not to in edge_rev_index: + edge_rev_index[to] = [] + edge_rev_index[to].append(_from) + return edge_rev_index + + +def fix_session_management_graph(plan): + edge_rev_index = build_rev_index(plan) + blocks_added = {'logout': {}, 'jump_to_admin': {}} + + # Find blocks that have 2 ancestors and one of them is logout or promote_to_admin, then merge all those branches + has_errors = False + for node in plan.nodes(): + if len(edge_rev_index.get(node, [])) < 2: + continue + + block_type = None + old_block = None + if 'logout' in edge_rev_index[node]: + old_block = block_type = 'logout' + if 'promote_to_admin' in edge_rev_index[node]: + old_block = 'promote_to_admin' + block_type = 'jump_to_admin' + + if block_type is None: + # Nothing can be done here to fix it. Maybe later + # `prune_incorrect_dependencies()` will fix it. + # Otherwise it will throw an exception. + continue + + other_block = [ + block for block in edge_rev_index[node] if block != old_block + ][0] + plan.remove_edge(old_block, node) + plan.remove_edge(other_block, node) + if other_block not in blocks_added[block_type]: + block_id = gen_id() + blocks_added[block_type][other_block] = block_id + plan.add_node(block_id, label=block_type) + plan.add_edge(other_block, block_id) + + plan.add_edge(blocks_added[block_type][other_block], node) + + +def prune_incorrect_dependencies(plan): + edge_rev_index = build_rev_index(plan) + + # Build equivalences table + equivs = {} + for node in plan.nodes(): + op = plan.get_node(node).attr['label'] + if op not in equivs: + equivs[op] = [] + equivs[op].append(node) + + edge_rev_index = build_rev_index(plan) + + # Check that blocks have only zero or one ancestor + has_errors = False + for node in plan.nodes(): + if len(edge_rev_index.get(node, [])) > 1: + op = plan.get_node(node).attr['label'] + has_option = False + for eq in equivs[op]: + if len(edge_rev_index.get(op, [])) < 2: + has_option = True + break + + if has_option: + plan.remove_node(node) + else: + logging.error("Node has multiple ancestors: {}".format(op)) + has_errors = True + + assert not has_errors + + +if __name__ == '__main__': + api_groups = parse(read_permission_matrix(DATA_FILE)) + add_api_requirements(api_groups) + test_plan = build_test_plan(api_groups) + logging.getLogger().setLevel(logging.INFO) + + logging.info('Base plan: {} operations'.format(len(test_plan.nodes()))) + + test_plan.tred() + fix_session_management_graph(test_plan) + prune_incorrect_dependencies(test_plan) + test_plan.write('plan.gv') + test_plan.draw('plan.svg', prog='dot') + logging.info('Final plan: {} operations'.format(len(test_plan.nodes()))) diff --git a/utils/testing/requirements.txt b/utils/testing/requirements.txt new file mode 100644 index 00000000..04413ff0 --- /dev/null +++ b/utils/testing/requirements.txt @@ -0,0 +1,3 @@ +pygraphviz +requests +colorama diff --git a/utils/testing/run-api-test.sh b/utils/testing/run-api-test.sh new file mode 100644 index 00000000..4525700f --- /dev/null +++ b/utils/testing/run-api-test.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +cd "$(dirname "$0")" + +IMAGE="$1" +if [ -z "$IMAGE" ];then + echo "$0 " + exit 1 +fi + +pip3 install -U --user -r requirements.txt || exit 1 + +cleanup(){ + docker rm -f "$DOCKER" +} + + +case "${CI_TYPE}" in + gitlab) + # Consider: https://stackoverflow.com/a/54252215 + export DOCKER=`docker run --rm -d -p 8888:8888 "$IMAGE"` + DOCKER_HOST="docker" # Given by DockerINDocker + ;; + + *) + # Expect local execution + export DOCKER=`docker run --rm -d "$IMAGE"` + DOCKER_HOST=`docker exec "$DOCKER" hostname -i|tr -d '\r\n'` +esac + +export API_TEST_ROOT="http://${DOCKER_HOST}:8888/api/v0" +export API_TEST_DOCKER="$DOCKER" + +python3 gen_test_api_plan.py || cleanup + +(docker logs -f "$DOCKER" > api_test_logs.txt; echo "Docker stopped" >&2) & + +# Wait for the API to be ready +for i in `seq 1 60`;do + curl -s "${API_TEST_ROOT}/ping" >>/dev/null && break + printf '.' >&2 + sleep 1 +done + +curl -s "${API_TEST_ROOT}/ping" >>/dev/null +if [ $? -ne 0 ];then + cleanup + exit 1 +fi + +python3 run_plan.py plan.gv + +ECODE=$? + +cleanup +exit $ECODE diff --git a/utils/testing/run_plan.py b/utils/testing/run_plan.py new file mode 100644 index 00000000..9ca2bf3e --- /dev/null +++ b/utils/testing/run_plan.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 + +import copy +import json +import os +import subprocess +import sys +import time +import traceback + +import pygraphviz as pgv +import requests +from colorama import Back, Fore, Style + +import builder +import gen_test_api_plan as test_api + +api_groups = test_api.parse(test_api.read_permission_matrix( + test_api.DATA_FILE)) +test_api.add_api_requirements(api_groups) + +ENDPOINTS = {} + +for content in api_groups.values(): + for item in content['items']: + ENDPOINTS[item['url']] = item + + +def reset_ctx(ctx): + del ctx['token'] + + +def promote_to_admin(ctx): + out = subprocess.check_output([ + "docker", "exec", ctx['docker'], "/app/scripts/run_erl.sh", + "{ok, {user, UserId}} = automate_storage:get_userid_from_username(<<\"" + + ctx['user_name'] + "\">>)," + + "automate_storage:promote_user_to_admin(UserId)" + ]).decode('utf-8').strip() + + if out != 'ok': + raise AssertionError("Expected 'ok', found {}".format(out)) + + +def get_user_id_from_data(ctx): + headers = {'Authorization': ctx['token']} + res = requests.post('{}/sessions/login'.format(ctx['root'].strip('/')), + json={ + 'password': ctx['password'], + 'username': ctx['user_name'], + }, + headers=headers) + data = res.json() + return (data['user_id'], data['token']) + + +def negate_execute_op(op, ctx): + failed = False + try: + res = execute_op(op, ctx) + if res == 'ignored': + return res + except: + failed = True + + if not failed: + raise AssertionError('Operation expected to fail, but succeeded') + + +def execute_op(op, ctx): + if op == 'promote_to_admin': + promote_to_admin(ctx) + return + if op == 'logout': + reset_ctx(ctx) + return 0 + if op == 'jump_to_admin': + ctx['token'] = ctx['_admin_token'] + return 0 + + headers = {} + if 'token' in ctx: + headers['Authorization'] = ctx['token'] + + if op == 'make-editor': + user_id, new_token = get_user_id_from_data(ctx) + res = requests.post('{}/groups/by-id/{}/collaborators'.format( + ctx['root'].strip('/'), ctx['group_id']), + json={ + 'action': + 'invite', + 'collaborators': [{ + 'id': user_id, + 'role': 'editor' + }] + }, + headers=headers) + res.raise_for_status() + ctx['user_id'] = user_id + ctx['token'] = new_token + return 2 + if op == 'make-viewer': + user_id, new_token = get_user_id_from_data(ctx) + res = requests.post('{}/groups/by-id/{}/collaborators'.format( + ctx['root'].strip('/'), ctx['group_id']), + json={ + 'action': + 'invite', + 'collaborators': [{ + 'id': user_id, + 'role': 'viewer' + }] + }, + headers=headers) + res.raise_for_status() + ctx['user_id'] = user_id + ctx['token'] = new_token + return 2 + + verb, endpoint = op.split() + path = ctx['root'].strip('/') + '/' + endpoint.strip('/') + if verb == 'get': + data = builder.build_data_for_query(verb, endpoint, ctx) + path = builder.fill_path_params(path, ctx) + res = requests.get(path, headers=headers, params=data) + res.raise_for_status() + + elif verb == 'post': + data = builder.build_data_for_query(verb, endpoint, ctx) + path = builder.fill_path_params(path, ctx) + if isinstance(data, tuple): + res = requests.post(path, + json=data[0], + files=data[1], + headers=headers) + else: + res = requests.post(path, json=data, headers=headers) + res.raise_for_status() + builder.update_ctx(verb, endpoint, res, ctx) + + elif verb == 'delete': # TODO Will be required to happen after all ops on resource happened + return 'ignored' + # data = builder.build_data_for_query(verb, endpoint, ctx) + # path = builder.fill_path_params(path, ctx) + # res = requests.delete(path, json=data, headers=headers) + # res.raise_for_status() + # builder.update_ctx(verb, endpoint, res, ctx) + + elif verb == 'patch': # TODO + return 'ignored' + + elif verb == 'put': # TODO + return 'ignored' + + elif verb == 'write': # WS verb + return 'ignored' + + elif verb == 'read': # WS verb + return 'ignored' + + elif verb in ('check_noauth', 'mayget'): + if verb == 'check_noauth': + tested_verbs = 'get', 'post', 'delete', 'put', 'patch' + elif verb == 'mayget': + tested_verbs = 'post', 'delete', 'put', 'patch' # No GET tested + + test_ctx = copy.deepcopy(ctx) + for test_verb in tested_verbs: + data = builder.build_data_for_query(test_verb, endpoint, test_ctx) + path = builder.fill_path_params(path, test_ctx) + if test_verb == 'get': + res = requests.get(path, headers=headers, params=data) + elif isinstance(data, tuple): + res = requests.__dict__[test_verb](path, + json=data[0], + files=data[1], + headers=headers) + else: + res = requests.__dict__[test_verb](path, + json=data, + headers=headers) + + if res.ok: + raise Exception( + "Auth passed (incorrectly). Verb: {}".format(test_verb)) + return len(tested_verbs) + else: + raise Exception('Unknown verb: ' + verb) + + +def gen_admin_user(ctx): + ctx = copy.deepcopy(ctx) + execute_op('POST /sessions/register'.lower(), ctx) + execute_op('POST /sessions/login'.lower(), ctx) + execute_op('promote_to_admin', ctx) + return ( + ctx['token'], + ctx['user_id'], + ctx['user_name'], + ) + + +def main(test_plan): + ctx = { + 'root': os.getenv('API_TEST_ROOT', 'http://localhost:8881/api/v0'), + 'docker': os.getenv('API_TEST_DOCKER', 'back-test-docker'), + } + + plan = pgv.AGraph(test_plan, strict=False, directed=True) + results = pgv.AGraph(strict=False, directed=True) + results.graph_attr['rankdir'] = 'LR' + results.node_attr['shape'] = 'rect' + + edge_idx = {} + for (_from, to) in plan.edges(): + if not _from in edge_idx: + edge_idx[_from] = [] + edge_idx[_from].append(to) + + skipped_nodes = set(plan.nodes()) + + rem_nodes = [('start', 'start', ctx)] + skipped_nodes.remove('start') + + endpoints_failed = [] + + times = [] + ignored = [] + test_start = time.time() + + admin = gen_admin_user(ctx) + ctx['_admin_token'], ctx['_admin_id'], ctx['_admin_name'] = admin + run_node_count = 0 + succeeded_count = 0 + + while len(rem_nodes) > 0: + (node, node_id, ctx) = rem_nodes.pop() + + # Don't allow one branch context to affect another branch. + # + # Note that this context will be passed by reference to the next nodes + # and after that it will be modified in place by the current node's + # operation. + ctx = copy.deepcopy(ctx) + + lines = plan.get_node(node).attr['label'].split('\n') + op = lines[0] + tags = {} + label = op + + if len(lines) > 1: + tags = json.loads('\n'.join(lines[1:]).strip()) + + if 'section' in tags: + label = '{} [{}]'.format(op, tags['section']) + + try: + t0 = time.time() + tags['auth'] = bool(ctx.get('token', None)) + if op.upper() != 'START': + run_node_count += 1 + print("\r\x1b[K … {} {} ".format(op, tags), flush=True, end='') + + if op.lower().startswith('fail-'): + result = negate_execute_op(op[5:].lower(), ctx) + else: + result = execute_op(op.lower(), ctx) + + test_time = time.time() - t0 + if result is None or isinstance(result, int): + succeeded_count += 1 + if isinstance(result, int): + for i in range(result): + times.append((op, test_time / result)) + else: + times.append((op, test_time)) + + print("\r{} ✓ {} [{:.03f}s] {}".format( + Fore.GREEN, op, test_time, tags), + Style.RESET_ALL, + end='') + results.add_node(node_id, + label=label, + style='filled', + fillcolor='green', + color='black', + fontsize=14, + fontcolor='white') + elif result == 'ignored': + ignored.append(op) + print( + "\r{} I {} [{:.03f}s] {}".format( + Fore.BLUE, op, test_time, tags, node), + Style.RESET_ALL) + results.add_node(node_id, + label=label, + style='filled', + fillcolor='lightblue', + color='black', + fontsize=14, + fontcolor='black') + else: + results.add_node(node_id, shape='doublecircle') + except Exception as ex: + print( + "\r{} × {} [{:.03f}s] {}".format(Fore.RED, op, + time.time() - t0, tags), + Style.RESET_ALL) + print(traceback.format_exc()) + results.add_node(node_id, + label=label, + style='filled', + shape='signature', + fillcolor='red', + color='black', + fontsize=16, + fontcolor='white') + results.draw('results.svg', prog='dot') + endpoints_failed.append(op) + continue + + for tgt in edge_idx.get(node, []): + tgt_id = test_api.gen_id() + + if node_id is not None: + results.add_edge(node_id, tgt_id) + + rem_nodes.append((tgt, tgt_id, ctx)) + skipped_nodes.discard(tgt) + + num_nodes = len(plan.nodes()) - 1 # For the "start" node + num_calls = len(times) + test_time = time.time() - test_start + + print("\r\x1b[K", end="") + print("{} nodes NOT tested ({:.2%})".format(len(skipped_nodes), + len(skipped_nodes) / + num_nodes)) + print("{} paths FAILED ({:.2%})".format(len(endpoints_failed), + len(endpoints_failed) / num_nodes)) + print("{} nodes IGNORED ({:.2%})".format(len(ignored), + len(ignored) / num_nodes)) + print("{} nodes SUCCEEDED ({:.2%})".format(succeeded_count, + succeeded_count / num_nodes)) + + print( + "{} calls from {} nodes completed in {:.02f} seconds. (Avg: {:.03f}s)". + format(num_calls, run_node_count - len(ignored), test_time, + test_time / num_calls)) + + print("10 Slowest operations:") + deduped = {} + for (test, duration) in times: + if test not in deduped: + deduped[test] = duration + elif deduped[test] < duration: + deduped[test] = duration + + for (test, duration) in sorted(deduped.items(), + key=lambda x: x[1], + reverse=True)[:10]: + print(' {:.03f}s: {}'.format(duration, test)) + + results.draw('results.svg', prog='dot') + if len(endpoints_failed) == 0: + return 0 + else: + return 1 + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print("{} ".format(sys.argv[0])) + exit(2) + exit(main(sys.argv[1]))