diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore deleted file mode 100644 index 5bf9577..0000000 --- a/.devcontainer/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/data/ -/www/ \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bf214b3..5b09ae3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,42 @@ { - "name": "laravel-fresh-install", + "name": "Anythink Development Container", + "image": "public.ecr.aws/v0a2l7y2/wilco/anythink-devcontainer:latest", + "forwardPorts": [3000, 3001, 5433, 27017], + "portsAttributes": { + "3000": { + "label": "Backend", + "elevateIfNeeded": true, + "requireLocalPort": true, + "onAutoForward": "silent" + }, + "3001": { + "label": "Frontend", + "elevateIfNeeded": true, + "requireLocalPort": true, + "onAutoForward": "silent" + }, + "5433": { + "label": "Database", + "elevateIfNeeded": true, + "requireLocalPort": true, + "onAutoForward": "silent" + }, + "27017": { + "label": "Database", + "elevateIfNeeded": true, + "requireLocalPort": true, + "onAutoForward": "silent" + } + }, + "postStartCommand": "bash -c .devcontainer/setup.sh", - // "xdebug.php-debug" = official XDEBUG extension - "customizations": { - "vscode": { - "extensions": [ - "xdebug.php-debug" - ] - } - }, - - "forwardPorts": [80], - - // execute our one-time repo init if /vendor/ does not exist - "postCreateCommand": "sh init_repo.sh" + "settings": { + "extensions.ignoreRecommendations": true, + "workbench.startupEditor": "none", + "workbench.colorTheme": "Visual Studio Dark", + "workbench.colorCustomizations": {}, + "workbench.welcomePage.walkthroughs.openOnInstall": false, + "workbench.welcomePage.experimental.videoTutorials": "off", + "github.codespaces.defaultExtensions": [] + } } diff --git a/.devcontainer/open_port.sh b/.devcontainer/open_port.sh new file mode 100755 index 0000000..7358769 --- /dev/null +++ b/.devcontainer/open_port.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +RETRY_COUNT=3 +RETRY_INTERVAL=5 +TARGET_PORTS=("3000" "3001") + +sleep 5 + +for port in "${TARGET_PORTS[@]}"; do + for ((try = 1; try <= RETRY_COUNT; try++)); do + echo "Attempt $try: Making port $port public." + + gh codespace ports visibility $port:public -c $CODESPACE_NAME + sleep 1 + + ports_json=$(gh codespace ports -c $CODESPACE_NAME --json label,sourcePort,visibility) + visibility=$(echo "$ports_json" | jq -r ".[] | select(.sourcePort == $port) | .visibility") + + if [ "$visibility" == "public" ]; then + echo "Port $port is now public." + break + elif [ $try -lt $RETRY_COUNT ]; then + echo "Port $port is still not public. Retrying in $RETRY_INTERVAL seconds..." + sleep $RETRY_INTERVAL + else + echo "Failed to make port $port public after $RETRY_COUNT attempts." + fi + done +done diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100755 index 0000000..cf397d6 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,25 @@ +WILCO_ID="`cat .wilco`" +ENGINE_EVENT_ENDPOINT="${ENGINE_BASE_URL}/users/${WILCO_ID}/event" +CODESPACE_BACKEND_HOST="${CODESPACE_NAME}-3000.preview.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" +CODESPACE_BACKEND_URL="https://${CODESPACE_BACKEND_HOST}" + +# Update engine that codespace started for user +curl -L -X POST "${ENGINE_EVENT_ENDPOINT}" -H "Content-Type: application/json" --data-raw "{ \"event\": \"github_codespace_started\" }" + +# Export backend envs when in codespaces +echo "export CODESPACE_BACKEND_HOST=\"${CODESPACE_BACKEND_HOST}\"" >> ~/.bashrc +echo "export CODESPACE_BACKEND_URL=\"${CODESPACE_BACKEND_URL}\"" >> ~/.bashrc +echo "export CODESPACE_WDS_SOCKET_PORT=443" >> ~/.bashrc + + +# Change backend port visibility to public +echo "(&>/dev/null .devcontainer/open_port.sh &)" >> ~/.bashrc + + +# Export welcome prompt in bash: +echo "printf \"\n\n☁️☁️☁️️ Anythink: Develop in the Cloud ☁️☁️☁️\n\"" >> ~/.bashrc +echo "printf \"\n=============================================\n\"" >> ~/.bashrc +echo "gh codespace ports -c $CODESPACE_NAME" >> ~/.bashrc +echo "printf \"=============================================\n\"" >> ~/.bashrc +echo "printf \"(Once docker-compose is up and running, you can access the frontend and backend using the above urls)\n\"" >> ~/.bashrc +echo "printf \"\n\x1b[31m \x1b[1m👉 Type: \\\`docker-compose up\\\` to run the project. 👈\n\n\"" >> ~/.bashrc diff --git a/.docker/nginx/conf.d/error_reporting.ini b/.docker/nginx/conf.d/error_reporting.ini deleted file mode 100644 index d040e65..0000000 --- a/.docker/nginx/conf.d/error_reporting.ini +++ /dev/null @@ -1 +0,0 @@ -error_reporting=E_ALL \ No newline at end of file diff --git a/.docker/nginx/conf.d/nginx-webserver.conf b/.docker/nginx/conf.d/nginx-webserver.conf deleted file mode 100644 index 7a184e3..0000000 --- a/.docker/nginx/conf.d/nginx-webserver.conf +++ /dev/null @@ -1,38 +0,0 @@ -# this server config works with Laravel too -server { - listen 80; - listen [::]:80 default_server; - - # not needed for now - # server_name example.com; - - root /var/www/htdoc/public; - - add_header X-Frame-Options "SAMEORIGIN"; - add_header X-Content-Type-Options "nosniff"; - - index index.php; - - charset utf-8; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location = /favicon.ico { access_log off; log_not_found off; } - location = /robots.txt { access_log off; log_not_found off; } - - error_page 404 /index.php; - - location ~* \.php$ { - fastcgi_pass php:9000; - - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - } - - location ~ /\.(?!well-known).* { - deny all; - } -} \ No newline at end of file diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile deleted file mode 100644 index 36217f8..0000000 --- a/.docker/php/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -# defined in docker-compose.yml, from docker-env.env -ARG RUNTIME_PHP_IMAGE - -# Use the specified image as the base -FROM ${RUNTIME_PHP_IMAGE} - -# Update the packages -# Install system packages required for MongoDB extension -# 'mysql-client' so we can log into mysql from the PHP container with the command 'mysql -h mysql -u root -p' where mysql is the service name -# 'iputils-ping' to get the ping command -RUN apt-get update && apt-get install -y libssl-dev wget git unzip default-mysql-client iputils-ping - -RUN pecl apt update \ - && apt install libzip-dev -y \ - && docker-php-ext-install zip \ - && rm -rf /var/lib/apt/lists/* - -# Required for MySQL to work in PHP -RUN docker-php-ext-install mysqli && \ - docker-php-ext-install pdo_mysql - -# Test if already installed and -# install the mongodb PHP extension -# RUN pecl install mongodb && docker-php-ext-enable mongodb -RUN bash -c '[[ -n "$(pecl list | grep mongodb)" ]]\ - || (pecl install mongodb && docker-php-ext-enable mongodb)' - -# Test if already installed and -# install and enable XDEBUG -# RUN pecl install xdebug && docker-php-ext-enable xdebug -RUN bash -c '[[ -n "$(pecl list | grep xdebug)" ]]\ - || (pecl install xdebug && docker-php-ext-enable xdebug)' - -# install Redis PHP driver -RUN pecl install -o -f redis \ -&& rm -rf /tmp/pear \ -&& docker-php-ext-enable redis \ -&& docker-php-ext-enable pdo_mysql - -# Task: copy rep's PHP .ini files to be automatically parsed -# -# directory is related to the PHP service context -# dot NOT use ./filename.ext for root files -# use filename.ext -COPY .docker/php/docker-php.ini /usr/local/etc/php/conf.d/ - -COPY .docker/php/xdebug.ini /usr/local/etc/php/conf.d/ - -# Get Composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -WORKDIR /var/www/htdoc diff --git a/.docker/php/docker-php.ini b/.docker/php/docker-php.ini deleted file mode 100644 index fa19660..0000000 --- a/.docker/php/docker-php.ini +++ /dev/null @@ -1 +0,0 @@ -# hubert stuff \ No newline at end of file diff --git a/.docker/php/xdebug.ini b/.docker/php/xdebug.ini deleted file mode 100644 index cdf2de0..0000000 --- a/.docker/php/xdebug.ini +++ /dev/null @@ -1,41 +0,0 @@ -; already loaded in /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini -;zend_extension=xdebug - -; FIXME : should be elsewhere, like docker-php.ini -error_log = /var/log/php-errors.log -catch_workers_output = yes - -[xdebug] -; 'debug' means we're enabling step-by-step debugging -xdebug.mode=debug - -xdebug.client_port=9003 - -; xdebug.client_host is the IP address of the system where VS Code runs -; that IP address is DIFFERENT depending on WHERE VS Code is launched in Windows/Mac, WSL, Container/devcontainer/codespaces -; -; the PHP container sends debugging data OUT to xdebug.client_host:xdebug.client_port - - -; localhost is used when running btoh VS Code and PHP from within **the same PHP container** -; after opening the project in the Container -xdebug.client_host=localhost - -; if using Docker Desktop 'host.docker.internal' is supposed to hold the IP Address of -; the Docker host, but that's not always true. DOUBLE-CHECK - -;xdebug.client_host=host.docker.internal - -; 'yes': This will always initiate a debugging, profiling, or tracing session as soon as a request is received, without needing any specific trigger -; 'no' : This will never initiate a session regardless of the presence of any trigger -; 'trigger' : This will initiate a session only if a specific trigger, like a GET/POST variable or a cookie, is present in the request. -xdebug.start_with_request=yes - -; OPTIONAL: idekey -; in the browser add a URL param , if not using a browser utility -; url.to.debug?XDEBUG_SESSION_START=PHPSTORM -; sets up a coockie called "XDEBUG_SESSION_START" with the value "PHPSTORM", which is the "trigger" -; xdebug.idekey=PHPSTORM - -; defines a log file. This is created, with touch, and initialized (permissions) in the PHP container Dockerfile -xdebug.log=/tmp/xdebug.log \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..8ebf549 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,3 @@ +# Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml new file mode 100644 index 0000000..9a56d6a --- /dev/null +++ b/.github/workflows/k8s.yml @@ -0,0 +1,157 @@ +name: Build and deploy to Kubernetes +on: + push: + branches: + - main + +concurrency: + group: k8s + cancel-in-progress: true + +jobs: + check-kubernetes-enabled: + runs-on: ubuntu-20.04 + outputs: + kubernetes-enabled: ${{ steps.kubernetes-flag-defined.outputs.DEFINED }} + steps: + - id: kubernetes-flag-defined + if: "${{ env.ENABLE_KUBERNETES != '' }}" + run: echo "DEFINED=true" >> $GITHUB_OUTPUT + env: + ENABLE_KUBERNETES: ${{ secrets.ENABLE_KUBERNETES }} + + check-secret: + runs-on: ubuntu-20.04 + needs: [check-kubernetes-enabled] + outputs: + aws-creds-defined: ${{ steps.aws-creds-defined.outputs.DEFINED }} + kubeconfig-defined: ${{ steps.kubeconfig-defined.outputs.DEFINED }} + if: needs.check-kubernetes-enabled.outputs.kubernetes-enabled == 'true' + steps: + - id: aws-creds-defined + if: "${{ env.AWS_ACCESS_KEY_ID != '' && env.AWS_SECRET_ACCESS_KEY != '' }}" + run: echo "DEFINED=true" >> $GITHUB_OUTPUT + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - id: kubeconfig-defined + if: "${{ env.KUBECONFIG != '' }}" + run: echo "DEFINED=true" >> $GITHUB_OUTPUT + env: + KUBECONFIG: ${{ secrets.KUBECONFIG }} + + build-backend: + name: Build backend image + runs-on: ubuntu-20.04 + needs: [check-secret] + if: needs.check-secret.outputs.aws-creds-defined == 'true' + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Set the image tag + run: echo IMAGE_TAG=${GITHUB_REPOSITORY/\//-}-latest >> $GITHUB_ENV + + - name: Set repository name + run: | + if [ ${{ secrets.CLUSTER_ENV }} == 'staging' ]; then + echo "REPO_NAME=staging-anythink-backend" >> $GITHUB_ENV + else + echo "REPO_NAME=anythink-backend" >> $GITHUB_ENV + fi + + - name: Build, tag, and push backend image to Amazon ECR + id: build-image-backend + run: | + docker build \ + -t ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO_NAME }}:${{ env.IMAGE_TAG }} \ + -f backend/Dockerfile.aws \ + . + docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO_NAME }}:${{ env.IMAGE_TAG }} + + build-frontend: + name: Build frontend images + runs-on: ubuntu-20.04 + needs: [check-secret] + if: needs.check-secret.outputs.aws-creds-defined == 'true' + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Set the image tag + run: echo IMAGE_TAG=${GITHUB_REPOSITORY/\//-}-latest >> $GITHUB_ENV + + - name: Set repository name + run: | + if [ ${{ secrets.CLUSTER_ENV }} == 'staging' ]; then + echo "REPO_NAME=staging-anythink-frontend" >> $GITHUB_ENV + else + echo "REPO_NAME=anythink-frontend" >> $GITHUB_ENV + fi + + - name: Build, tag, and push frontend image to Amazon ECR + id: build-image-frontend + run: | + docker build \ + -t ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO_NAME }}:${{ env.IMAGE_TAG }} \ + -f frontend/Dockerfile.aws \ + . + docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO_NAME }}:${{ env.IMAGE_TAG }} + + deploy: + name: Deploy latest tag using helm + runs-on: ubuntu-20.04 + if: needs.check-secret.outputs.kubeconfig-defined == 'true' + needs: + - build-frontend + - build-backend + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Create kube config + run: | + mkdir -p $HOME/.kube/ + echo "${{ secrets.KUBECONFIG }}" > $HOME/.kube/config + chmod 600 $HOME/.kube/config + - name: Install helm + run: | + curl -LO https://get.helm.sh/helm-v3.8.0-linux-amd64.tar.gz + tar -zxvf helm-v3.8.0-linux-amd64.tar.gz + mv linux-amd64/helm /usr/local/bin/helm + helm version + - name: Lint helm charts + run: helm lint ./charts/ + + - name: Set the image tag + run: echo IMAGE_TAG=${GITHUB_REPOSITORY/\//-}-latest >> $GITHUB_ENV + + - name: Deploy + run: | + helm upgrade --install --timeout 10m anythink-market ./charts/ \ + --set clusterEnv=${{ secrets.CLUSTER_ENV }} \ + --set frontend.image.tag=${{ env.IMAGE_TAG }} \ + --set backend.image.tag=${{ env.IMAGE_TAG }} diff --git a/.github/workflows/wilco-actions.yml b/.github/workflows/wilco-actions.yml new file mode 100644 index 0000000..15fa886 --- /dev/null +++ b/.github/workflows/wilco-actions.yml @@ -0,0 +1,34 @@ +on: + pull_request: + branches: + - main + +jobs: + wilco: + runs-on: ubuntu-20.04 + timeout-minutes: 10 + name: Pr checks + + steps: + - name: Check out project + uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: "16" + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.6.0 + with: + mongodb-version: "4.4" + + - uses: oNaiPs/secrets-to-env-action@v1 + with: + secrets: ${{ toJSON(secrets) }} + + - name: Wilco checks + id: Wilco + uses: trywilco/actions@main + with: + engine: ${{ secrets.WILCO_ENGINE_URL }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10d146d --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/backend/node_modules +/frontend/node_modules +/.wilco-helpers/node_modules +/tests/e2e/node_modules +/tests/frontend/node_modules/ +/tests/frontend/test-results/ +/tests/frontend/playwright-report/ +/tests/frontend/playwright/.cache/ + +/.pnp +.pnp.js + +# testing +/coverage + +# production +/backend/build +/frontend/build + +# misc +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +#IDEs +/.idea/ diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 628f34b..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Listen for Xdebug", - "type": "php", - "request": "launch", - "port": 9003, - // ${workspaceFolder} == directory where /.vscode/ is - // the syntax is SERVER_PATH : LOCAL_PATH - "pathMappings": { - "/var/www/htdoc": "${workspaceFolder}/src" - } - }, - { - "name": "Launch currently open script", - "type": "php", - "request": "launch", - "program": "${file}", - "cwd": "${fileDirname}", - "port": 0, - "runtimeArgs": [ - "-dxdebug.start_with_request=yes" - ], - "env": { - "XDEBUG_MODE": "debug,develop", - "XDEBUG_CONFIG": "client_port=${port}" - } - }, - { - "name": "Launch Built-in web server", - "type": "php", - "request": "launch", - "runtimeArgs": [ - "-dxdebug.mode=debug", - "-dxdebug.start_with_request=yes", - "-S", - "localhost:0" - ], - "program": "", - "cwd": "${workspaceRoot}", - "port": 9003, - "serverReadyAction": { - "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", - "uriFormat": "http://localhost:%s", - "action": "openExternally" - } - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0c878f1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "workbench.startupEditor": "none" +} diff --git a/DockerSetup.md b/DockerSetup.md deleted file mode 100644 index a346fc7..0000000 --- a/DockerSetup.md +++ /dev/null @@ -1,35 +0,0 @@ -# proposed directory structure to integrate Docker and .devcontainer - -/repository-root -│ -├── .devcontainer/ -│ ├── devcontainer.json -│ └── Dockerfile -│ -├── docker/ -│ ├── nginx/ -│ │ └── default.conf -│ ├── mysql/ -│ │ └── my.cnf -│ └── php/ -│ └── Dockerfile -│ -├── docker-compose.yml -│ -├── src/ -│ ├── app/ -│ ├── bootstrap/ -│ ├── config/ -│ ├── database/ -│ ├── public/ -│ ├── resources/ -│ ├── routes/ -│ ├── storage/ -│ ├── tests/ -│ ├── .env.example -│ ├── .gitignore -│ ├── composer.json -│ ├── phpunit.xml -│ └── ... -│ -└── README.md \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 7d47c6c..0000000 --- a/README.md +++ /dev/null @@ -1,7 +0,0 @@ -## About this repository - -- A clean Laravel 10 project created with `composer create-project laravel/laravel` -- no extra package added yet -- designed to run in CodeSpaces or Docker via `\.devcontainer\` with `docker compuse up` -- directory structure friendly to Wilco via the `\.framework\` directory -- NGINX serves the public web folder located at `\.framework\php\src\public\` diff --git a/anythink_ack.sh b/anythink_ack.sh new file mode 100755 index 0000000..8deeb51 --- /dev/null +++ b/anythink_ack.sh @@ -0,0 +1,12 @@ +#!/bin/sh +sleep 10s + +echo "Welcome to" +echo " _ _ _ _ _ " +echo " / \ _ __ _ _ | |_ | |__ (_) _ __ | | __ " +echo " / _ \ | '_ \ | | | | | __| | '_ \ | | | '_ \ | |/ / " +echo " / ___ \ | | | | | |_| | | |_ | | | | | | | | | | | < " +echo " /_/ \_\ |_| |_| \__, | \__| |_| |_| |_| |_| |_| |_|\_\ " +echo " |___/ " + +echo '\n\e]8;;https://app.wilco.gg/chat\e\\Click here\e]8;;\e\\ to go back to Snack' diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..a812403 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +.DS_Store + +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +.idea diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..e779cce --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1 @@ +FROM public.ecr.aws/v0a2l7y2/wilco/anythink-backend-node:latest diff --git a/backend/Dockerfile.aws b/backend/Dockerfile.aws new file mode 100644 index 0000000..7ce8e4a --- /dev/null +++ b/backend/Dockerfile.aws @@ -0,0 +1,10 @@ +FROM node:16 + +WORKDIR /usr/src +COPY backend ./backend +COPY .wilco ./.wilco + +# Pre-install npm packages +WORKDIR /usr/src/backend +RUN yarn install + diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..14f890a --- /dev/null +++ b/backend/README.md @@ -0,0 +1,22 @@ +# Anythink Market Backend + +The Anythink Market backend is Node web app written with [Express](https://expressjs.com/) + +## Dependencies + +- [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) - For generating JWTs used by authentication +- [mongoose](https://github.com/Automattic/mongoose) - For modeling and mapping MongoDB data to javascript +- [mongoose-unique-validator](https://github.com/blakehaswell/mongoose-unique-validator) - For handling unique validation errors in Mongoose. Mongoose only handles validation at the document level, so a unique index across a collection will throw an exception at the driver level. The `mongoose-unique-validator` plugin helps us by formatting the error like a normal mongoose `ValidationError`. +- [passport](https://github.com/jaredhanson/passport) - For handling user authentication +- [slug](https://github.com/dodo/node-slug) - For encoding titles into a URL-friendly format + +## Application Structure + +- `app.js` - The entry point to our application. This file defines our express server and connects it to MongoDB using mongoose. It also requires the routes and models we'll be using in the application. +- `config/` - This folder contains configuration for passport as well as a central location for configuration/environment variables. +- `routes/` - This folder contains the route definitions for our API. +- `models/` - This folder contains the schema definitions for our Mongoose models. + +## Error Handling + +In `routes/api/index.js`, we define a error-handling middleware for handling Mongoose's `ValidationError`. This middleware will respond with a 422 status code and format the response to have [error messages the clients can understand](https://github.com/gothinkster/realworld/blob/master/API.md#errors-and-status-codes) diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 0000000..f4e7f35 --- /dev/null +++ b/backend/app.js @@ -0,0 +1,89 @@ +require("dotenv").config(); +var http = require("http"), + path = require("path"), + methods = require("methods"), + express = require("express"), + bodyParser = require("body-parser"), + session = require("express-session"), + cors = require("cors"), + passport = require("passport"), + errorhandler = require("errorhandler"), + mongoose = require("mongoose"); + +var isProduction = process.env.NODE_ENV === "production"; + +// Create global app object +var app = express(); + +app.use(cors()); + +// Normal express config defaults +app.use(require("morgan")("dev")); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); + +app.use(require("method-override")()); +app.use(express.static(__dirname + "/public")); + +app.use( + session({ + secret: "secret", + cookie: { maxAge: 60000 }, + resave: false, + saveUninitialized: false + }) +); + +if (!isProduction) { + app.use(errorhandler()); +} + +if (!process.env.MONGODB_URI) { + console.warn("Missing MONGODB_URI in env, please add it to your .env file"); +} + +mongoose.connect(process.env.MONGODB_URI); +if (isProduction) { +} else { + mongoose.set("debug", true); +} + +require("./models/User"); +require("./models/Item"); +require("./models/Comment"); +require("./config/passport"); + +app.use(require("./routes")); + +/// catch 404 and forward to error handler +app.use(function (req, res, next) { + if (req.url === "/favicon.ico") { + res.writeHead(200, { "Content-Type": "image/x-icon" }); + res.end(); + } else { + const err = new Error("Not Found"); + err.status = 404; + next(err); + } +}); + +/// error handler +app.use(function(err, req, res, next) { + console.log(err.stack); + if (isProduction) { + res.sendStatus(err.status || 500) + } else { + res.status(err.status || 500); + res.json({ + errors: { + message: err.message, + error: err + } + }); + } +}); + +// finally, let's start our server... +var server = app.listen(process.env.PORT || 3000, function() { + console.log("Listening on port " + server.address().port); +}); diff --git a/backend/config/index.js b/backend/config/index.js new file mode 100644 index 0000000..1bf9d6a --- /dev/null +++ b/backend/config/index.js @@ -0,0 +1,3 @@ +module.exports = { + secret: process.env.NODE_ENV === 'production' ? process.env.SECRET : 'secret' +}; diff --git a/backend/config/passport.js b/backend/config/passport.js new file mode 100644 index 0000000..abe0ce2 --- /dev/null +++ b/backend/config/passport.js @@ -0,0 +1,18 @@ +var passport = require('passport'); +var LocalStrategy = require('passport-local').Strategy; +var mongoose = require('mongoose'); +var User = mongoose.model('User'); + +passport.use(new LocalStrategy({ + usernameField: 'user[email]', + passwordField: 'user[password]' +}, function(email, password, done) { + User.findOne({email: email}).then(function(user){ + if(!user || !user.validPassword(password)){ + return done(null, false, {errors: {'email or password': 'is invalid'}}); + } + + return done(null, user); + }).catch(done); +})); + diff --git a/backend/lib/event.js b/backend/lib/event.js new file mode 100644 index 0000000..48a270e --- /dev/null +++ b/backend/lib/event.js @@ -0,0 +1,25 @@ +const axiosLib = require("axios"); +const fs = require("fs"); + +const WILCO_ID = process.env.WILCO_ID || fs.readFileSync('../.wilco', 'utf8') +const baseURL = process.env.ENGINE_BASE_URL || "https://engine.wilco.gg" + +const axios = axiosLib.create({ + baseURL: baseURL, + headers: { + 'Content-type': 'application/json', + }, +}); + +async function sendEvent(event, metadata) { + try { + const result = await axios.post(`/users/${WILCO_ID}/event`, JSON.stringify({event, metadata})); + return result.data; + } catch (error) { + console.error(`failed to send event ${event} to Wilco engine`) + } +} + +module.exports = { + sendEvent, +} diff --git a/backend/models/Comment.js b/backend/models/Comment.js new file mode 100644 index 0000000..995c6c0 --- /dev/null +++ b/backend/models/Comment.js @@ -0,0 +1,22 @@ +var mongoose = require("mongoose"); + +var CommentSchema = new mongoose.Schema( + { + body: String, + seller: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, + item: { type: mongoose.Schema.Types.ObjectId, ref: "Item" } + }, + { timestamps: true } +); + +// Requires population of seller +CommentSchema.methods.toJSONFor = function(user) { + return { + id: this._id, + body: this.body, + createdAt: this.createdAt, + seller: this.seller.toProfileJSONFor(user) + }; +}; + +mongoose.model("Comment", CommentSchema); diff --git a/backend/models/Item.js b/backend/models/Item.js new file mode 100644 index 0000000..96421a3 --- /dev/null +++ b/backend/models/Item.js @@ -0,0 +1,62 @@ +var mongoose = require("mongoose"); +var uniqueValidator = require("mongoose-unique-validator"); +var slug = require("slug"); +var User = mongoose.model("User"); + +var ItemSchema = new mongoose.Schema( + { + slug: { type: String, lowercase: true, unique: true }, + title: {type: String, required: [true, "can't be blank"]}, + description: {type: String, required: [true, "can't be blank"]}, + image: String, + favoritesCount: { type: Number, default: 0 }, + comments: [{ type: mongoose.Schema.Types.ObjectId, ref: "Comment" }], + tagList: [{ type: String }], + seller: { type: mongoose.Schema.Types.ObjectId, ref: "User" } + }, + { timestamps: true } +); + +ItemSchema.plugin(uniqueValidator, { message: "is already taken" }); + +ItemSchema.pre("validate", function(next) { + if (!this.slug) { + this.slugify(); + } + + next(); +}); + +ItemSchema.methods.slugify = function() { + this.slug = + slug(this.title) + + "-" + + ((Math.random() * Math.pow(36, 6)) | 0).toString(36); +}; + +ItemSchema.methods.updateFavoriteCount = function() { + var item = this; + + return User.count({ favorites: { $in: [item._id] } }).then(function(count) { + item.favoritesCount = count; + + return item.save(); + }); +}; + +ItemSchema.methods.toJSONFor = function(user) { + return { + slug: this.slug, + title: this.title, + description: this.description, + image: this.image, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + tagList: this.tagList, + favorited: user ? user.isFavorite(this._id) : false, + favoritesCount: this.favoritesCount, + seller: this.seller.toProfileJSONFor(user) + }; +}; + +mongoose.model("Item", ItemSchema); diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000..8616f03 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,130 @@ +var mongoose = require("mongoose"); +var uniqueValidator = require("mongoose-unique-validator"); +var crypto = require("crypto"); +var jwt = require("jsonwebtoken"); +var secret = require("../config").secret; + +var UserSchema = new mongoose.Schema( + { + username: { + type: String, + lowercase: true, + unique: true, + required: [true, "can't be blank"], + match: [/^[a-zA-Z0-9]+$/, "is invalid"], + index: true + }, + email: { + type: String, + lowercase: true, + unique: true, + required: [true, "can't be blank"], + match: [/\S+@\S+\.\S+/, "is invalid"], + index: true + }, + bio: String, + image: String, + role: { + type: String, + enum: ["user", "admin"], + default: "user" + }, + favorites: [{ type: mongoose.Schema.Types.ObjectId, ref: "Item" }], + following: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], + hash: String, + salt: String + }, + { timestamps: true } +); + +UserSchema.plugin(uniqueValidator, { message: "is already taken." }); + +UserSchema.methods.validPassword = function(password) { + var hash = crypto + .pbkdf2Sync(password, this.salt, 10000, 512, "sha512") + .toString("hex"); + return this.hash === hash; +}; + +UserSchema.methods.setPassword = function(password) { + this.salt = crypto.randomBytes(16).toString("hex"); + this.hash = crypto + .pbkdf2Sync(password, this.salt, 10000, 512, "sha512") + .toString("hex"); +}; + +UserSchema.methods.generateJWT = function() { + var today = new Date(); + var exp = new Date(today); + exp.setDate(today.getDate() + 60); + + return jwt.sign( + { + id: this._id, + username: this.username, + exp: parseInt(exp.getTime() / 1000) + }, + secret + ); +}; + +UserSchema.methods.toAuthJSON = function() { + return { + username: this.username, + email: this.email, + token: this.generateJWT(), + bio: this.bio, + image: this.image, + role: this.role + }; +}; + +UserSchema.methods.toProfileJSONFor = function(user) { + return { + username: this.username, + bio: this.bio, + image: + this.image || "https://static.productionready.io/images/smiley-cyrus.jpg", + following: user ? user.isFollowing(this._id) : false + }; +}; + +UserSchema.methods.favorite = function(id) { + if (this.favorites.indexOf(id) === -1) { + this.favorites = this.favorites.concat([id]); + } + + return this.save(); +}; + +UserSchema.methods.unfavorite = function(id) { + this.favorites.remove(id); + return this.save(); +}; + +UserSchema.methods.isFavorite = function(id) { + return this.favorites.some(function(favoriteId) { + return favoriteId.toString() === id.toString(); + }); +}; + +UserSchema.methods.follow = function(id) { + if (this.following.indexOf(id) === -1) { + this.following = this.following.concat([id]); + } + + return this.save(); +}; + +UserSchema.methods.unfollow = function(id) { + this.following.remove(id); + return this.save(); +}; + +UserSchema.methods.isFollowing = function(id) { + return this.following.some(function(followId) { + return followId.toString() === id.toString(); + }); +}; + +mongoose.model("User", UserSchema); diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..4dcd6eb --- /dev/null +++ b/backend/package.json @@ -0,0 +1,42 @@ +{ + "name": "anythink-market-backend", + "version": "1.0.0", + "main": "app.js", + "engines": { + "node": "^16" + }, + "scripts": { + "start": "node ./app.js", + "dev": "nodemon ./app.js", + "seeds": "node ./scripts/seeds.js", + "test": "newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json", + "stop": "lsof -ti :3000 | xargs kill" + }, + "dependencies": { + "axios": "^0.25.0", + "body-parser": "1.15.0", + "cors": "2.7.1", + "dotenv": "^8.2.0", + "ejs": "2.4.1", + "errorhandler": "1.4.3", + "express": "4.13.4", + "express-async-handler": "^1.2.0", + "express-jwt": "3.3.0", + "express-session": "1.13.0", + "jsonwebtoken": "7.1.9", + "method-override": "2.3.5", + "methods": "1.1.2", + "mongoose": "5.12.5", + "mongoose-unique-validator": "^3.0.0", + "morgan": "1.7.0", + "passport": "0.3.2", + "passport-local": "1.0.0", + "request": "2.69.0", + "slug": "0.9.1", + "underscore": "1.8.3" + }, + "devDependencies": { + "newman": "^3.8.2", + "nodemon": "^1.11.0" + } +} diff --git a/src/public/favicon.ico b/backend/public/.keep similarity index 100% rename from src/public/favicon.ico rename to backend/public/.keep diff --git a/backend/routes/api/index.js b/backend/routes/api/index.js new file mode 100644 index 0000000..380d027 --- /dev/null +++ b/backend/routes/api/index.js @@ -0,0 +1,23 @@ +var router = require('express').Router(); + +router.use('/', require('./users')); +router.use('/profiles', require('./profiles')); +router.use('/items', require('./items')); +router.use('/tags', require('./tags')); +router.use('/ping', require('./ping')); + +router.use(function(err, req, res, next){ + if(err.name === 'ValidationError'){ + return res.status(422).json({ + errors: Object.keys(err.errors).reduce(function(errors, key){ + errors[key] = err.errors[key].message; + + return errors; + }, {}) + }); + } + + return next(err); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/api/items.js b/backend/routes/api/items.js new file mode 100644 index 0000000..84a8af9 --- /dev/null +++ b/backend/routes/api/items.js @@ -0,0 +1,331 @@ +var router = require("express").Router(); +var mongoose = require("mongoose"); +var Item = mongoose.model("Item"); +var Comment = mongoose.model("Comment"); +var User = mongoose.model("User"); +var auth = require("../auth"); +const { sendEvent } = require("../../lib/event"); + +// Preload item objects on routes with ':item' +router.param("item", function(req, res, next, slug) { + Item.findOne({ slug: slug }) + .populate("seller") + .then(function(item) { + if (!item) { + return res.sendStatus(404); + } + + req.item = item; + + return next(); + }) + .catch(next); +}); + +router.param("comment", function(req, res, next, id) { + Comment.findById(id) + .then(function(comment) { + if (!comment) { + return res.sendStatus(404); + } + + req.comment = comment; + + return next(); + }) + .catch(next); +}); + +router.get("/", auth.optional, function(req, res, next) { + var query = {}; + var limit = 100; + var offset = 0; + + if (typeof req.query.limit !== "undefined") { + limit = req.query.limit; + } + + if (typeof req.query.offset !== "undefined") { + offset = req.query.offset; + } + + if (typeof req.query.tag !== "undefined") { + query.tagList = { $in: [req.query.tag] }; + } + + Promise.all([ + req.query.seller ? User.findOne({ username: req.query.seller }) : null, + req.query.favorited ? User.findOne({ username: req.query.favorited }) : null + ]) + .then(function(results) { + var seller = results[0]; + var favoriter = results[1]; + + if (seller) { + query.seller = seller._id; + } + + if (favoriter) { + query._id = { $in: favoriter.favorites }; + } else if (req.query.favorited) { + query._id = { $in: [] }; + } + + return Promise.all([ + Item.find(query) + .limit(Number(limit)) + .skip(Number(offset)) + .sort({ createdAt: "desc" }) + .exec(), + Item.count(query).exec(), + req.payload ? User.findById(req.payload.id) : null + ]).then(async function(results) { + var items = results[0]; + var itemsCount = results[1]; + var user = results[2]; + return res.json({ + items: await Promise.all( + items.map(async function(item) { + item.seller = await User.findById(item.seller); + return item.toJSONFor(user); + }) + ), + itemsCount: itemsCount + }); + }); + }) + .catch(next); +}); + +router.get("/feed", auth.required, function(req, res, next) { + var limit = 20; + var offset = 0; + + if (typeof req.query.limit !== "undefined") { + limit = req.query.limit; + } + + if (typeof req.query.offset !== "undefined") { + offset = req.query.offset; + } + + User.findById(req.payload.id).then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + Promise.all([ + Item.find({ seller: { $in: user.following } }) + .limit(Number(limit)) + .skip(Number(offset)) + .populate("seller") + .exec(), + Item.count({ seller: { $in: user.following } }) + ]) + .then(function(results) { + var items = results[0]; + var itemsCount = results[1]; + + return res.json({ + items: items.map(function(item) { + return item.toJSONFor(user); + }), + itemsCount: itemsCount + }); + }) + .catch(next); + }); +}); + +router.post("/", auth.required, function(req, res, next) { + User.findById(req.payload.id) + .then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + var item = new Item(req.body.item); + + item.seller = user; + + return item.save().then(function() { + sendEvent('item_created', { item: req.body.item }) + return res.json({ item: item.toJSONFor(user) }); + }); + }) + .catch(next); +}); + +// return a item +router.get("/:item", auth.optional, function(req, res, next) { + Promise.all([ + req.payload ? User.findById(req.payload.id) : null, + req.item.populate("seller").execPopulate() + ]) + .then(function(results) { + var user = results[0]; + + return res.json({ item: req.item.toJSONFor(user) }); + }) + .catch(next); +}); + +// update item +router.put("/:item", auth.required, function(req, res, next) { + User.findById(req.payload.id).then(function(user) { + if (req.item.seller._id.toString() === req.payload.id.toString()) { + if (typeof req.body.item.title !== "undefined") { + req.item.title = req.body.item.title; + } + + if (typeof req.body.item.description !== "undefined") { + req.item.description = req.body.item.description; + } + + if (typeof req.body.item.image !== "undefined") { + req.item.image = req.body.item.image; + } + + if (typeof req.body.item.tagList !== "undefined") { + req.item.tagList = req.body.item.tagList; + } + + req.item + .save() + .then(function(item) { + return res.json({ item: item.toJSONFor(user) }); + }) + .catch(next); + } else { + return res.sendStatus(403); + } + }); +}); + +// delete item +router.delete("/:item", auth.required, function(req, res, next) { + User.findById(req.payload.id) + .then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + if (req.item.seller._id.toString() === req.payload.id.toString()) { + return req.item.remove().then(function() { + return res.sendStatus(204); + }); + } else { + return res.sendStatus(403); + } + }) + .catch(next); +}); + +// Favorite an item +router.post("/:item/favorite", auth.required, function(req, res, next) { + var itemId = req.item._id; + + User.findById(req.payload.id) + .then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + return user.favorite(itemId).then(function() { + return req.item.updateFavoriteCount().then(function(item) { + return res.json({ item: item.toJSONFor(user) }); + }); + }); + }) + .catch(next); +}); + +// Unfavorite an item +router.delete("/:item/favorite", auth.required, function(req, res, next) { + var itemId = req.item._id; + + User.findById(req.payload.id) + .then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + return user.unfavorite(itemId).then(function() { + return req.item.updateFavoriteCount().then(function(item) { + return res.json({ item: item.toJSONFor(user) }); + }); + }); + }) + .catch(next); +}); + +// return an item's comments +router.get("/:item/comments", auth.optional, function(req, res, next) { + Promise.resolve(req.payload ? User.findById(req.payload.id) : null) + .then(function(user) { + return req.item + .populate({ + path: "comments", + populate: { + path: "seller" + }, + options: { + sort: { + createdAt: "desc" + } + } + }) + .execPopulate() + .then(function(item) { + return res.json({ + comments: req.item.comments.map(function(comment) { + return comment.toJSONFor(user); + }) + }); + }); + }) + .catch(next); +}); + +// create a new comment +router.post("/:item/comments", auth.required, function(req, res, next) { + User.findById(req.payload.id) + .then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + var comment = new Comment(req.body.comment); + comment.item = req.item; + comment.seller = user; + + return comment.save().then(function() { + req.item.comments = req.item.comments.concat([comment]); + + return req.item.save().then(function(item) { + res.json({ comment: comment.toJSONFor(user) }); + }); + }); + }) + .catch(next); +}); + +router.delete("/:item/comments/:comment", auth.required, function( + req, + res, + next +) { + req.item.comments.remove(req.comment._id); + req.item + .save() + .then( + Comment.find({ _id: req.comment._id }) + .remove() + .exec() + ) + .then(function() { + res.sendStatus(204); + }); +}); + +module.exports = router; diff --git a/backend/routes/api/ping.js b/backend/routes/api/ping.js new file mode 100644 index 0000000..a327948 --- /dev/null +++ b/backend/routes/api/ping.js @@ -0,0 +1,19 @@ +const router = require("express").Router(); +const asyncHandler = require("express-async-handler"); +const auth = require("../auth"); +const { sendEvent } = require("../../lib/event"); + +router.get("/", + auth.optional, + asyncHandler(async (req, res) => { + + try { + const result = await sendEvent('ping') + return res.json(result); + } catch (e) { + console.error(e) + return res.sendStatus(500); + } + })); + +module.exports = router; diff --git a/backend/routes/api/profiles.js b/backend/routes/api/profiles.js new file mode 100644 index 0000000..ffcd833 --- /dev/null +++ b/backend/routes/api/profiles.js @@ -0,0 +1,53 @@ +var router = require('express').Router(); +var mongoose = require('mongoose'); +var User = mongoose.model('User'); +var auth = require('../auth'); + +// Preload user profile on routes with ':username' +router.param('username', function(req, res, next, username){ + User.findOne({username: username}).then(function(user){ + if (!user) { return res.sendStatus(404); } + + req.profile = user; + + return next(); + }).catch(next); +}); + +router.get('/:username', auth.optional, function(req, res, next){ + if(req.payload){ + User.findById(req.payload.id).then(function(user){ + if(!user){ return res.json({profile: req.profile.toProfileJSONFor(false)}); } + + return res.json({profile: req.profile.toProfileJSONFor(user)}); + }); + } else { + return res.json({profile: req.profile.toProfileJSONFor(false)}); + } +}); + +router.post('/:username/follow', auth.required, function(req, res, next){ + var profileId = req.profile._id; + + User.findById(req.payload.id).then(function(user){ + if (!user) { return res.sendStatus(401); } + + return user.follow(profileId).then(function(){ + return res.json({profile: req.profile.toProfileJSONFor(user)}); + }); + }).catch(next); +}); + +router.delete('/:username/follow', auth.required, function(req, res, next){ + var profileId = req.profile._id; + + User.findById(req.payload.id).then(function(user){ + if (!user) { return res.sendStatus(401); } + + return user.unfollow(profileId).then(function(){ + return res.json({profile: req.profile.toProfileJSONFor(user)}); + }); + }).catch(next); +}); + +module.exports = router; diff --git a/backend/routes/api/tags.js b/backend/routes/api/tags.js new file mode 100644 index 0000000..2090495 --- /dev/null +++ b/backend/routes/api/tags.js @@ -0,0 +1,12 @@ +var router = require('express').Router(); +var mongoose = require('mongoose'); +var Item = mongoose.model('Item'); + +// return a list of tags +router.get('/', function(req, res, next) { + Item.find().distinct('tagList').then(function(tags){ + return res.json({tags: tags}); + }).catch(next); +}); + +module.exports = router; diff --git a/backend/routes/api/users.js b/backend/routes/api/users.js new file mode 100644 index 0000000..aeae77f --- /dev/null +++ b/backend/routes/api/users.js @@ -0,0 +1,90 @@ +var mongoose = require("mongoose"); +var router = require("express").Router(); +var passport = require("passport"); +var User = mongoose.model("User"); +var auth = require("../auth"); +const { sendEvent } = require("../../lib/event"); + +router.get("/user", auth.required, function(req, res, next) { + User.findById(req.payload.id) + .then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + return res.json({ user: user.toAuthJSON() }); + }) + .catch(next); +}); + +router.put("/user", auth.required, function(req, res, next) { + User.findById(req.payload.id) + .then(function(user) { + if (!user) { + return res.sendStatus(401); + } + + // only update fields that were actually passed... + if (typeof req.body.user.username !== "undefined") { + user.username = req.body.user.username; + } + if (typeof req.body.user.email !== "undefined") { + user.email = req.body.user.email; + } + if (typeof req.body.user.bio !== "undefined") { + user.bio = req.body.user.bio; + } + if (typeof req.body.user.image !== "undefined") { + user.image = req.body.user.image; + } + if (typeof req.body.user.password !== "undefined") { + user.setPassword(req.body.user.password); + } + + return user.save().then(function() { + return res.json({ user: user.toAuthJSON() }); + }); + }) + .catch(next); +}); + +router.post("/users/login", function(req, res, next) { + if (!req.body.user.email) { + return res.status(422).json({ errors: { email: "can't be blank" } }); + } + + if (!req.body.user.password) { + return res.status(422).json({ errors: { password: "can't be blank" } }); + } + + passport.authenticate("local", { session: false }, function(err, user, info) { + if (err) { + return next(err); + } + + if (user) { + user.token = user.generateJWT(); + return res.json({ user: user.toAuthJSON() }); + } else { + return res.status(422).json(info); + } + })(req, res, next); +}); + +router.post("/users", function(req, res, next) { + var user = new User(); + + user.username = req.body.user.username; + user.email = req.body.user.email; + user.setPassword(req.body.user.password); + + user + .save() + .then(function() { + sendEvent('user_created', { username: req.body.user.username }) + return res.json({ user: user.toAuthJSON() }); + }) + .catch(next); +}); + +module.exports = router; diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..e44a215 --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,27 @@ +var jwt = require('express-jwt'); +var secret = require('../config').secret; + +function getTokenFromHeader(req){ + if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token' || + req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { + return req.headers.authorization.split(' ')[1]; + } + + return null; +} + +var auth = { + required: jwt({ + secret: secret, + userProperty: 'payload', + getToken: getTokenFromHeader + }), + optional: jwt({ + secret: secret, + userProperty: 'payload', + credentialsRequired: false, + getToken: getTokenFromHeader + }) +}; + +module.exports = auth; diff --git a/backend/routes/index.js b/backend/routes/index.js new file mode 100644 index 0000000..81d38f9 --- /dev/null +++ b/backend/routes/index.js @@ -0,0 +1,13 @@ +var router = require('express').Router(); + +router.get('/', (req, res, next) => { + res.send("Anythink backend is up."); +}); + +router.get('/health', (req, res, next) => { + res.sendStatus("200"); +}) + +router.use('/api', require('./api')); + +module.exports = router; diff --git a/backend/scripts/seeds.js b/backend/scripts/seeds.js new file mode 100644 index 0000000..4989da1 --- /dev/null +++ b/backend/scripts/seeds.js @@ -0,0 +1 @@ +//TODO: seeds script should come here, so we'll be able to put some data in our local env diff --git a/backend/seeds.sh b/backend/seeds.sh new file mode 100755 index 0000000..855f73d --- /dev/null +++ b/backend/seeds.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +yarn seeds diff --git a/backend/start.sh b/backend/start.sh new file mode 100755 index 0000000..bca7355 --- /dev/null +++ b/backend/start.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +yarn start diff --git a/backend/tests/api-tests.postman.json b/backend/tests/api-tests.postman.json new file mode 100644 index 0000000..fcfaf1a --- /dev/null +++ b/backend/tests/api-tests.postman.json @@ -0,0 +1,1900 @@ +{ + "variables": [], + "info": { + "name": "Anythink-Market API Tests", + "_postman_id": "dda3e595-02d7-bf12-2a43-3daea0970192", + "description": "Collection for testing the Anythink-Market API\n\nhttps://github.com/gothinkster/realworld", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [{ + "name": "Auth", + "description": "", + "item": [{ + "name": "Register", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if (!(environment.isIntegrationTest)) {", + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", + "", + "var user = responseJSON.user || {};", + "", + "tests['User has \"email\" property'] = user.hasOwnProperty('email');", + "tests['User has \"username\" property'] = user.hasOwnProperty('username');", + "tests['User has \"token\" property'] = user.hasOwnProperty('token');", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/users", + "method": "POST", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\", \"username\":\"johnjacob\"}}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Login", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", + "", + "var user = responseJSON.user || {};", + "", + "tests['User has \"email\" property'] = user.hasOwnProperty('email');", + "tests['User has \"username\" property'] = user.hasOwnProperty('username');", + "tests['User has \"token\" property'] = user.hasOwnProperty('token');", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/users/login", + "method": "POST", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\"}}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Login and Remember Token", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", + "", + "var user = responseJSON.user || {};", + "", + "tests['User has \"email\" property'] = user.hasOwnProperty('email');", + "tests['User has \"username\" property'] = user.hasOwnProperty('username');", + "tests['User has \"token\" property'] = user.hasOwnProperty('token');", + "", + "if(tests['User has \"token\" property']){", + " postman.setEnvironmentVariable('token', user.token);", + "}", + "", + "tests['Environment variable \"token\" has been set'] = environment.token === user.token;", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/users/login", + "method": "POST", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\"}}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Current User", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", + "", + "var user = responseJSON.user || {};", + "", + "tests['User has \"email\" property'] = user.hasOwnProperty('email');", + "tests['User has \"username\" property'] = user.hasOwnProperty('username');", + "tests['User has \"token\" property'] = user.hasOwnProperty('token');", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/user", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + }, + { + "name": "Update User", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", + "", + "var user = responseJSON.user || {};", + "", + "tests['User has \"email\" property'] = user.hasOwnProperty('email');", + "tests['User has \"username\" property'] = user.hasOwnProperty('username');", + "tests['User has \"token\" property'] = user.hasOwnProperty('token');", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/user", + "method": "PUT", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"user\":{\"email\":\"john@jacob.com\"}}" + }, + "description": "" + }, + "response": [] + } + ] + }, + { + "name": "Items with authentication", + "description": "", + "item": [{ + "name": "Feed", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/feed", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "All Items", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "All Items with auth", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Items by Author", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?seller=johnjacob", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "seller", + "value": "johnjacob" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Items by Author with auth", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?seller=johnjacob", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "seller", + "value": "johnjacob", + "equals": true, + "description": "" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + }, + { + "name": "Items Favorited by Username", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + " ", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?favorited=jane", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "favorited", + "value": "jane" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Items Favorited by Username with auth", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + " ", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?favorited=jane", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "favorited", + "value": "jane" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Items by Tag", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?tag=dragons", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "tag", + "value": "dragons" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Create Item", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", + "", + "var item = responseJSON.item || {};", + "", + "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + "if(tests['Item has \"slug\" property']){", + " postman.setEnvironmentVariable('slug', item.slug);", + "}", + "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items", + "method": "POST", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"item\":{\"title\":\"How to train your dragon\", \"description\":\"Ever wonder how?\", \"body\":\"Very carefully.\", \"tagList\":[\"dragons\",\"training\"]}}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Single Item by slug", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", + "", + "var item = responseJSON.item || {};", + "", + "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/{{slug}}", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + }, + { + "name": "Update Item", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if (!(environment.isIntegrationTest)) {", + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", + "", + "var item = responseJSON.item || {};", + "", + "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/{{slug}}", + "method": "PUT", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"item\":{\"body\":\"With two hands\"}}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Favorite Item", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", + "", + "var item = responseJSON.item || {};", + "", + "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + "tests[\"Item's 'favorited' property is true\"] = item.favorited === true;", + "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + "tests[\"Item's 'favoritesCount' property is greater than 0\"] = item.favoritesCount > 0;", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/{{slug}}/favorite", + "method": "POST", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Unfavorite Item", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", + "", + "var item = responseJSON.item || {};", + "", + "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + "tests[\"Item's \\\"favorited\\\" property is true\"] = item.favorited === false;", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/{{slug}}/favorite", + "method": "DELETE", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + } + ] + }, + { + "name": "Items", + "description": "", + "item": [{ + "name": "All Items", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Items by Author", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?seller=johnjacob", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "seller", + "value": "johnjacob" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Items Favorited by Username", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + " ", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?favorited=jane", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "favorited", + "value": "jane" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Items by Tag", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", + " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", + " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", + "", + " if(responseJSON.items.length){", + " var item = responseJSON.items[0];", + "", + " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + " } else {", + " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", + " }", + "}", + "" + ] + } + }], + "request": { + "url": { + "raw": "{{apiUrl}}/items?tag=dragons", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "items" + ], + "query": [{ + "key": "tag", + "value": "dragons" + }], + "variable": [] + }, + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Single Item by slug", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", + "", + "var item = responseJSON.item || {};", + "", + "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", + "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", + "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", + "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", + "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", + "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", + "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", + "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", + "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", + "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", + "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", + "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", + "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", + "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/{{slug}}", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + } + ] + }, + { + "name": "Comments", + "description": "", + "item": [{ + "name": "All Comments for Item", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"comments\" property'] = responseJSON.hasOwnProperty('comments');", + "", + " if(responseJSON.comments.length){", + " var comment = responseJSON.comments[0];", + "", + " tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');", + " tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');", + " tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');", + " tests['\"createdAt\" property is an ISO 8601 timestamp'] = new Date(comment.createdAt).toISOString() === comment.createdAt;", + " tests['Comment has \"updatedAt\" property'] = comment.hasOwnProperty('updatedAt');", + " tests['\"updatedAt\" property is an ISO 8601 timestamp'] = new Date(comment.updatedAt).toISOString() === comment.updatedAt;", + " tests['Comment has \"seller\" property'] = comment.hasOwnProperty('seller');", + " }", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/{{slug}}/comments", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + }, + { + "name": "Create Comment for Item", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "", + "tests['Response contains \"comment\" property'] = responseJSON.hasOwnProperty('comment');", + "", + "var comment = responseJSON.comment || {};", + "", + "tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');", + "tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');", + "tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');", + "tests['\"createdAt\" property is an ISO 8601 timestamp'] = new Date(comment.createdAt).toISOString() === comment.createdAt;", + "tests['Comment has \"seller\" property'] = comment.hasOwnProperty('seller');", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/items/{{slug}}/comments", + "method": "POST", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"comment\":{\"body\":\"Thank you so much!\"}}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Delete Comment for Item", + "request": { + "url": "{{apiUrl}}/items/{{slug}}/comments/1", + "method": "DELETE", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + } + ] + }, + { + "name": "Profiles", + "description": "", + "item": [{ + "name": "Profile", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if (!(environment.isIntegrationTest)) {", + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", + " ", + " var profile = responseJSON.profile || {};", + " ", + " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", + " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", + " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", + "}", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/profiles/johnjacob", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + }, + { + "name": "Follow Profile", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if (!(environment.isIntegrationTest)) {", + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", + " ", + " var profile = responseJSON.profile || {};", + " ", + " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", + " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", + " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", + " tests['Profile\\'s \"following\" property is true'] = profile.following === true;", + "}", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/profiles/johnjacob/follow", + "method": "POST", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\"user\":{\"email\":\"john@jacob.com\"}}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Unfollow Profile", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if (!(environment.isIntegrationTest)) {", + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + "", + " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", + " ", + " var profile = responseJSON.profile || {};", + " ", + " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", + " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", + " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", + " tests['Profile\\'s \"following\" property is false'] = profile.following === false;", + "}", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/profiles/johnjacob/follow", + "method": "DELETE", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + } + ] + }, + { + "name": "Tags", + "description": "", + "item": [{ + "name": "All Tags", + "event": [{ + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var is200Response = responseCode.code === 200;", + "", + "tests['Response code is 200 OK'] = is200Response;", + "", + "if(is200Response){", + " var responseJSON = JSON.parse(responseBody);", + " ", + " tests['Response contains \"tags\" property'] = responseJSON.hasOwnProperty('tags');", + " tests['\"tags\" property returned as array'] = Array.isArray(responseJSON.tags);", + "}", + "" + ] + } + }], + "request": { + "url": "{{apiUrl}}/tags", + "method": "GET", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }] + }, + { + "name": "Cleanup", + "description": "", + "item": [{ + "name": "Delete Item", + "request": { + "url": "{{apiUrl}}/items/{{slug}}", + "method": "DELETE", + "header": [{ + "key": "Content-Type", + "value": "application/json", + "description": "" + }, + { + "key": "X-Requested-With", + "value": "XMLHttpRequest", + "description": "" + }, + { + "key": "Authorization", + "value": "Token {{token}}", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + }] + } + ] +} diff --git a/backend/tests/env-api-tests.postman.json b/backend/tests/env-api-tests.postman.json new file mode 100644 index 0000000..3ba2ebf --- /dev/null +++ b/backend/tests/env-api-tests.postman.json @@ -0,0 +1,14 @@ +{ + "id": "4aa60b52-97fc-456d-4d4f-14a350e95dff", + "name": "Anythink-Market API Tests - Environment", + "values": [{ + "enabled": true, + "key": "apiUrl", + "value": "http://localhost:3000/api", + "type": "text" + }], + "timestamp": 1505871382668, + "_postman_variable_scope": "environment", + "_postman_exported_at": "2017-09-20T01:36:34.835Z", + "_postman_exported_using": "Postman/5.2.0" +} diff --git a/backend/yarn.lock b/backend/yarn.lock new file mode 100644 index 0000000..d00f449 --- /dev/null +++ b/backend/yarn.lock @@ -0,0 +1,3648 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/bson@*": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.2.0.tgz#a2f71e933ff54b2c3bf267b67fa221e295a33337" + integrity sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg== + dependencies: + bson "*" + +"@types/mongodb@^3.5.27": + version "3.6.20" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2" + integrity sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ== + dependencies: + "@types/bson" "*" + "@types/node" "*" + +"@types/node@*": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.0.tgz#62797cee3b8b497f6547503b2312254d4fe3c2bb" + integrity sha512-eMhwJXc931Ihh4tkU+Y7GiLzT/y/DBNpNtr4yU9O2w3SYBsr9NaOPhQlLKRmoWtI54uNwuo0IOUFQjVOTZYRvw== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.2.12: + version "1.2.13" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.2.13.tgz#e5f1f3928c6d95fd96558c36ec3d9d0de4a6ecea" + integrity sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo= + dependencies: + mime-types "~2.1.6" + negotiator "0.5.3" + +accepts@~1.3.0: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= + +ansi-align@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= + dependencies: + string-width "^2.0.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-uniq@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== + dependencies: + lodash "^4.17.10" + +async@^0.9.0: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= + +async@^1.4.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + +async@^2.0.1: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" + integrity sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w== + +aws4@^1.2.1, aws4@^1.6.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +axios@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" + integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== + dependencies: + follow-redirects "^1.14.7" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base64-url@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-1.2.1.tgz#199fd661702a0e7b7dcae6e0698bb089c52f6d78" + integrity sha1-GZ/WYXAqDnt9yubgaYuwicUvbXg= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + 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" + +basic-auth@~1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.0.4.tgz#030935b01de7c9b94a824b29f3fccb750d3a5290" + integrity sha1-Awk1sB3nyblKgksp8/zLdQ06UpA= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" + integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +bl@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.0.3.tgz#fc5421a28fd4226036c3b3891a66a25bc64d226e" + integrity sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4= + dependencies: + readable-stream "~2.0.5" + +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + +bluebird@^2.6.2: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" + integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= + +body-parser@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.0.tgz#8168abaeaf9e77e300f7b3aef4df4b46e9b21b35" + integrity sha1-gWirrq+ed+MA97Ou9N9LRumyGzU= + dependencies: + bytes "2.2.0" + content-type "~1.0.1" + debug "~2.2.0" + depd "~1.1.0" + http-errors "~1.4.0" + iconv-lite "0.4.13" + on-finished "~2.3.0" + qs "6.1.0" + raw-body "~2.1.5" + type-is "~1.6.11" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= + dependencies: + hoek "2.x.x" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + integrity sha1-T4owBctKfjiJ90kDD9JbluAdLjE= + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + integrity sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw== + dependencies: + hoek "4.x.x" + +boxen@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" + integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== + dependencies: + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" + widest-line "^2.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + 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" + +bson@*: + version "4.6.0" + resolved "https://registry.yarnpkg.com/bson/-/bson-4.6.0.tgz#15c3b39ba3940c3d915a0c44d51459f4b4fbf1b2" + integrity sha512-8jw1NU1hglS+Da1jDOUYuNcBJ4cNHCFIqzlwoFNnsTOg2R/ox0aTYcTiBN4dzRa9q7Cvy6XErh3L8ReTEb9AQQ== + dependencies: + buffer "^5.6.0" + +bson@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" + integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== + +btoa@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +bytes@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588" + integrity sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg= + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + integrity sha1-fZcZb51br39pNeJZhVSe3SpsIzk= + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + 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" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= + +camelcase@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + +capture-stack-trace@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" + integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + integrity sha1-cVuW6phBWTzDMGeSP17GDr2k99c= + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^1.1.0, chalk@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + 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" + +chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +charset@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/charset/-/charset-1.0.1.tgz#8d59546c355be61049a8fa9164747793319852bd" + integrity sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg== + +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + 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" + +ci-info@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" + integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== + +circular-json@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + integrity sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0= + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= + +cli-progress@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-1.8.0.tgz#5e8afc310f2058fbe33e9006e31c71c1c3b5da7f" + integrity sha1-Xor8MQ8gWPvjPpAG4xxxwcO12n8= + dependencies: + colors "^1.1.2" + +cli-table3@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.4.0.tgz#a7fd50f011d734e3f16403cfcbedbea97659e417" + integrity sha512-o0slI6EFJNI2aKE9jG1bVN6jXJG2vjzYsGhyd9RqRV/YiiEmzSwNNXb5qJmfLDSOdvfA6sUvdKVvi3p3Y1apxA== + dependencies: + kind-of "^3.0.4" + object-assign "^4.1.0" + string-width "^1.0.1" + optionalDependencies: + colors "^1.1.2" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +colors@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e" + integrity sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw== + +colors@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@~1.0.5: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" + integrity sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew== + +commander@^2.9.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +configstore@^3.0.0: + version "3.1.5" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.5.tgz#e9af331fadc14dabd544d3e7e76dc446a09a530f" + integrity sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA== + dependencies: + dot-prop "^4.2.1" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +content-disposition@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" + integrity sha1-h0dsamfI2qh+Muh2Ft+IO6f7Bxs= + +content-type@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.1.5.tgz#6ab9948a4b1ae21952cd2588530a4722d4044d7c" + integrity sha1-armUiksa4hlSzSWIUwpHItQETXw= + +cookie@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.2.3.tgz#1a59536af68537a21178a01346f87cb059d2ae5c" + integrity sha1-GllTavaFN6IReKATRvh8sFnSrlw= + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.7.1.tgz#3c2e50a58af9ef8c89bee21226b099be1f02739b" + integrity sha1-PC5QpYr574yJvuISJrCZvh8Cc5s= + dependencies: + vary "^1" + +crc@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.0.tgz#4258e351613a74ef1153dfcb05e820c3e9715d7f" + integrity sha1-QljjUWE6dO8RU9/LBeggw+lxXX8= + +create-error-class@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= + dependencies: + capture-stack-trace "^1.0.0" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= + dependencies: + boom "2.x.x" + +cryptiles@3.x.x: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.4.tgz#769a68c95612b56faadfcebf57ac86479cbe8322" + integrity sha512-8I1sgZHfVwcSOY6mSGpVU3lw/GSIZvusg8dD2+OGehCJpOhQRLNcH0qb9upQnOH4XhgxxFJSg6E2kx95deb1Tw== + dependencies: + boom "5.x.x" + +crypto-js@3.1.9-1: + version "3.1.9-1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8" + integrity sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg= + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= + +csv-parse@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-1.3.3.tgz#d1cfd8743c2f849a0abb2fd544db56695d19a490" + integrity sha1-0c/YdDwvhJoKuy/VRNtWaV0ZpJA= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +dbug@~0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/dbug/-/dbug-0.4.2.tgz#32b4b3105e8861043a6f9ac755d80e542d365b31" + integrity sha1-MrSzEF6IYQQ6b5rHVdgOVC02WzE= + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.6: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= + dependencies: + ms "0.7.1" + +decamelize@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +denque@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + +depd@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domelementtype@1, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" + integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ== + dependencies: + is-obj "^1.0.0" + +dotenv@^8.2.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" + integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +ejs@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.4.1.tgz#82e15b1b2a1f948b18097476ba2bd7c66f4d1566" + integrity sha1-guFbGyoflIsYCXR2uivXxm9NFWY= + +entities@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +errorhandler@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.4.3.tgz#b7b70ed8f359e9db88092f2d20c0f831420ad83f" + integrity sha1-t7cO2PNZ6duICS8tIMD4MUIK2D8= + dependencies: + accepts "~1.3.0" + escape-html "~1.0.3" + +escape-html@1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +etag@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" + integrity sha1-A9MLX2fdbmMtKUXTDWZScxo01dg= + +eventemitter3@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" + integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= + 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" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + 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" + +express-async-handler@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/express-async-handler/-/express-async-handler-1.2.0.tgz#ffc9896061d90f8d2e71a2d2b8668db5b0934391" + integrity sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w== + +express-jwt@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/express-jwt/-/express-jwt-3.3.0.tgz#d10e17244225b1968d20137ff77fc7488c88f494" + integrity sha1-0Q4XJEIlsZaNIBN/93/HSIyI9JQ= + dependencies: + async "^0.9.0" + express-unless "^0.3.0" + jsonwebtoken "^5.0.0" + lodash "~3.10.1" + +express-session@1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.13.0.tgz#8ac3b5c0188b48382851d88207b8e7746efb4011" + integrity sha1-isO1wBiLSDgoUdiCB7jndG77QBE= + dependencies: + cookie "0.2.3" + cookie-signature "1.0.6" + crc "3.4.0" + debug "~2.2.0" + depd "~1.1.0" + on-headers "~1.0.1" + parseurl "~1.3.0" + uid-safe "~2.0.0" + utils-merge "1.0.0" + +express-unless@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-0.3.1.tgz#2557c146e75beb903e2d247f9b5ba01452696e20" + integrity sha1-JVfBRudb65A+LSR/m1ugFFJpbiA= + +express@4.13.4: + version "4.13.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.13.4.tgz#3c0b76f3c77590c8345739061ec0bd3ba067ec24" + integrity sha1-PAt288d1kMg0VzkGHsC9O6Bn7CQ= + dependencies: + accepts "~1.2.12" + array-flatten "1.1.1" + content-disposition "0.5.1" + content-type "~1.0.1" + cookie "0.1.5" + cookie-signature "1.0.6" + debug "~2.2.0" + depd "~1.1.0" + escape-html "~1.0.3" + etag "~1.7.0" + finalhandler "0.4.1" + fresh "0.3.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.0.10" + qs "4.0.0" + range-parser "~1.0.3" + send "0.13.1" + serve-static "~1.10.2" + type-is "~1.6.6" + utils-merge "1.0.0" + vary "~1.0.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.0, extend@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + 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" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +file-type@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +filesize@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" + integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.4.1.tgz#85a17c6c59a94717d262d61230d4b0ebe3d4a14d" + integrity sha1-haF8bFmpRxfSYtYSMNSw6+PUoU0= + dependencies: + debug "~2.2.0" + escape-html "~1.0.3" + on-finished "~2.3.0" + unpipe "~1.0.0" + +follow-redirects@^1.14.7: + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~1.0.0-rc3: + version "1.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" + integrity sha1-rjFduaSQf6BlUCMEpm13M0de43w= + dependencies: + async "^2.0.1" + combined-stream "^1.0.5" + mime-types "^2.1.11" + +form-data@~2.3.1: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" + integrity sha1-ZR+DjiJCTnVm3hYdg1jKoZn4PU8= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +generate-function@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= + dependencies: + is-property "^1.0.0" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + 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" + +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= + dependencies: + ini "^1.3.4" + +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + +handlebars@4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" + integrity sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw= + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + integrity sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0= + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + integrity sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0= + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hawk@6.0.2, hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + integrity sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ== + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +hawk@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= + +hoek@4.x.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== + +htmlparser2@^3.9.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-errors@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" + integrity sha1-GX4izevUGYWF6GlO9nhhl7ke2UI= + dependencies: + inherits "~2.0.1" + statuses "1" + +http-errors@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf" + integrity sha1-bAJC3qaz33r9oVPHEImzHG6Cqr8= + dependencies: + inherits "2.0.1" + statuses ">= 1.2.1 < 2" + +http-reasons@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/http-reasons/-/http-reasons-0.1.0.tgz#a953ca670078669dde142ce899401b9d6e85d3b4" + integrity sha1-qVPKZwB4Zp3eFCzomUAbnW6F07Q= + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8= + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +httpntlm@1.7.6: + version "1.7.6" + resolved "https://registry.yarnpkg.com/httpntlm/-/httpntlm-1.7.6.tgz#6991e8352836007d67101b83db8ed0f915f906d0" + integrity sha1-aZHoNSg2AH1nEBuD247Q+RX5BtA= + dependencies: + httpreq ">=0.4.22" + underscore "~1.7.0" + +httpreq@>=0.4.22: + version "0.5.2" + resolved "https://registry.yarnpkg.com/httpreq/-/httpreq-0.5.2.tgz#be6777292fa1038d7771d7c01d9a5e1219de951c" + integrity sha512-2Jm+x9WkExDOeFRrdBCBSpLPT5SokTcRHkunV3pjKmX/cx6av8zQ0WtHUMDrYb6O4hBFzNU6sxJEypvRUVYKnw== + +iconv-lite@0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + integrity sha1-H4irpKsLFQjoMSrMOTRfNumS4vI= + +iconv-lite@0.4.22: + version "0.4.22" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.22.tgz#c6b16b9d05bc6c307dc9303a820412995d2eea95" + integrity sha512-1AinFBeDTnsvVEP+V1QBlHpM1UZZl7gWB6fcz7B1Ho+LI1dUh2sSrxoCfVt2PinRHzXAziSniEV3P7JbTDHcXA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.4, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +intel@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/intel/-/intel-1.2.0.tgz#11d1147eb6b3f4582bdf5337b37d541584e9e41e" + integrity sha1-EdEUfraz9Fgr31M3s31UFYTp5B4= + dependencies: + chalk "^1.1.0" + dbug "~0.4.2" + stack-trace "~0.0.9" + strftime "~0.10.0" + symbol "~0.3.1" + utcstring "~0.1.0" + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +ipaddr.js@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.0.5.tgz#5fa78cf301b825c78abc3042d812723049ea23c7" + integrity sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c= + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-ci@^1.0.10: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== + dependencies: + ci-info "^1.5.0" + +is-core-module@^2.2.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" + integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== + +is-my-json-valid@^2.12.4: + version "2.20.6" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz#a9d89e56a36493c77bda1440d69ae0dc46a08387" + integrity sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw== + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^5.0.0" + xtend "^4.0.0" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-property@^1.0.0, is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= + +is-retry-allowed@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + +is-stream@^1.0.0, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isemail@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" + integrity sha1-vgPfjMPineTSxd9lASY/H6RZXpo= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +joi@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" + integrity sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY= + dependencies: + hoek "2.x.x" + isemail "1.x.x" + moment "2.x.x" + topo "1.x.x" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonpointer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" + integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== + +jsonwebtoken@7.1.9: + version "7.1.9" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.1.9.tgz#847804e5258bec5a9499a8dc4a5e7a3bae08d58a" + integrity sha1-hHgE5SWL7FqUmajcSl56O64I1Yo= + dependencies: + joi "^6.10.1" + jws "^3.1.3" + lodash.once "^4.0.0" + ms "^0.7.1" + xtend "^4.0.1" + +jsonwebtoken@^5.0.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-5.7.0.tgz#1c90f9a86ce5b748f5f979c12b70402b4afcddb4" + integrity sha1-HJD5qGzlt0j1+XnBK3BAK0r83bQ= + dependencies: + jws "^3.0.0" + ms "^0.7.1" + xtend "^4.0.1" + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.0.0, jws@^3.1.3: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +kareem@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93" + integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.0.4, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +latest-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" + integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU= + dependencies: + package-json "^4.0.0" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= + +liquid-json@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/liquid-json/-/liquid-json-0.3.1.tgz#9155a18136d8a6b2615e5f16f9a2448ab6b50eea" + integrity sha1-kVWhgTbYprJhXl8W+aJEira1Duo= + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + +lodash.foreach@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= + +lodash.get@^4.0.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.mergewith@^4.6.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + +lodash@4.17.10: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== + +lodash@4.17.9: + version "4.17.9" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.9.tgz#9c056579af0bdbb4322e23c836df13ef2b271cb7" + integrity sha512-vuRLquvot5sKUldMBumG0YqLvX6m/RGBBOmqb3CWR/MC/QvvD1cTH1fOqxz2FJAQeoExeUdX5Gu9vP2EP6ik+Q== + +lodash@^4.17.10, lodash@^4.17.14: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lodash@~3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= + +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +marked@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66" + integrity sha512-tMsdNBgOsrUophCAFQl0XPe6Zqk/uy9gnue+jIIKhykO51hxyu6uNx7zBPy0+y/WKYVZZMspV9YeXLNdKk+iYw== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +method-override@2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/method-override/-/method-override-2.3.5.tgz#2cd5cdbff00c3673d7ae345119a812a5d95b8c8e" + integrity sha1-LNXNv/AMNnPXrjRRGagSpdlbjI4= + dependencies: + debug "~2.2.0" + methods "~1.1.1" + parseurl "~1.3.0" + vary "~1.0.1" + +methods@1.1.2, methods@~1.1.1, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + 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" + +mime-db@1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== + +mime-format@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mime-format/-/mime-format-2.0.0.tgz#e29f8891e284d78270246f0050d6834bdbbe1332" + integrity sha1-4p+IkeKE14JwJG8AUNaDS9u+EzI= + dependencies: + charset "^1.0.0" + +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== + dependencies: + mime-db "~1.33.0" + +mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.6, mime-types@~2.1.7: + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== + dependencies: + mime-db "1.51.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + integrity sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM= + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +moment@2.x.x: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +mongodb@3.6.6: + version "3.6.6" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.6.tgz#92e3658f45424c34add3003e3046c1535c534449" + integrity sha512-WlirMiuV1UPbej5JeCMqE93JRfZ/ZzqE7nJTwP85XzjAF4rRSeq2bGCb1cjfoHLOF06+HxADaPGqT0g3SbVT1w== + dependencies: + bl "^2.2.1" + bson "^1.1.4" + denque "^1.4.1" + optional-require "^1.0.2" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== + +mongoose-unique-validator@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mongoose-unique-validator/-/mongoose-unique-validator-3.1.0.tgz#10d6fa10ccf5515461e3b5693f193d227546d60b" + integrity sha512-UsBBlFapip8gc8x1h+nLWnkOy+GTy9Z+zmTyZ35icLV3EoLIVz180vJzepfMM9yBy2AJh+maeuoM8CWtqejGUg== + dependencies: + lodash.foreach "^4.1.0" + lodash.get "^4.0.2" + lodash.merge "^4.6.2" + +mongoose@5.12.5: + version "5.12.5" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.12.5.tgz#70d11d3e68a3aeeb6960262633e1ba80cb620385" + integrity sha512-VVoqiELZcoI2HhHDuPpfN3qmExrtIeXSWNb1nihf4w1SJoWGXilU/g2cQgeeSMc2vAHSZd5Nv2sNPvbZHFw+pg== + dependencies: + "@types/mongodb" "^3.5.27" + bson "^1.1.4" + kareem "2.3.2" + mongodb "3.6.6" + mongoose-legacy-pluralize "1.0.2" + mpath "0.8.3" + mquery "3.2.5" + ms "2.1.2" + regexp-clone "1.0.0" + safe-buffer "5.2.1" + sift "7.0.1" + sliced "1.0.1" + +morgan@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.7.0.tgz#eb10ca8e50d1abe0f8d3dad5c0201d052d981c62" + integrity sha1-6xDKjlDRq+D409rVwCAdBS2YHGI= + dependencies: + basic-auth "~1.0.3" + debug "~2.2.0" + depd "~1.1.0" + on-finished "~2.3.0" + on-headers "~1.0.1" + +mpath@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.8.3.tgz#828ac0d187f7f42674839d74921970979abbdd8f" + integrity sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA== + +mquery@3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.5.tgz#8f2305632e4bb197f68f60c0cffa21aaf4060c51" + integrity sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A== + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "^1.0.0" + safe-buffer "5.1.2" + sliced "1.0.1" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + integrity sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg= + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^0.7.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" + integrity sha1-cIFVpeROM/X9D8U+gdDUCpG+H/8= + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nan@^2.12.1: + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + 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" + +negotiator@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.5.3.tgz#269d5c476810ec92edbe7b6c2f28316384f9a7e8" + integrity sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g= + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +newman@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/newman/-/newman-3.10.0.tgz#24bb43963e25bb79a4fc158cd76bf20eaa179f06" + integrity sha512-8dr3kUedx/D4a/tiysvEjEQ+D+lLA/sgPASN33AiRyTKtdqzeVFuuBZYb3Jb+0TBd84Y3Qk8t24GuTY22HJN4g== + dependencies: + async "2.6.1" + cli-progress "1.8.0" + cli-table3 "0.4.0" + colors "1.3.0" + commander "2.16.0" + csv-parse "1.3.3" + eventemitter3 "3.1.0" + filesize "3.6.1" + handlebars "4.0.11" + lodash "4.17.9" + mkdirp "0.5.1" + parse-json "4.0.0" + postman-collection "3.1.1" + postman-collection-transformer "2.5.10" + postman-request "2.86.1-postman.1" + postman-runtime "7.2.0" + pretty-ms "3.2.0" + semver "5.5.0" + serialised-error "1.1.3" + shelljs "0.8.2" + word-wrap "1.2.3" + xmlbuilder "10.0.0" + +node-oauth1@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/node-oauth1/-/node-oauth1-1.2.2.tgz#fffb2813a88c2770711332ad0e5487b4927644a4" + integrity sha512-f2XC7Y68wJq6+s+LJn/yUq5Gqg9Y9zwIz2zY6vUyS8xzawnSWhXKOMJepLwvptjPl8IjVxtWh7iI9dbdKGSw4g== + dependencies: + crypto-js "3.1.9-1" + +node-uuid@~1.4.7: + version "1.4.8" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" + integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= + +nodemon@^1.11.0: + version "1.19.4" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.4.tgz#56db5c607408e0fdf8920d2b444819af1aae0971" + integrity sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ== + dependencies: + chokidar "^2.1.8" + 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 "^2.5.0" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.8.0, oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@^1.1.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" + integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optional-require@^1.0.2: + version "1.1.8" + resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.1.8.tgz#16364d76261b75d964c482b2406cb824d8ec44b7" + integrity sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA== + dependencies: + require-at "^1.0.6" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +package-json@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" + integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0= + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +parse-json@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-ms@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-1.0.1.tgz#56346d4749d78f23430ca0c713850aef91aa361d" + integrity sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0= + +parseurl@~1.3.0, parseurl@~1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +passport-local@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= + dependencies: + passport-strategy "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= + +passport@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.3.2.tgz#9dd009f915e8fe095b0124a01b8f82da07510102" + integrity sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI= + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss@^6.0.14: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + +postman-collection-transformer@2.5.10: + version "2.5.10" + resolved "https://registry.yarnpkg.com/postman-collection-transformer/-/postman-collection-transformer-2.5.10.tgz#cecf07b7cdac58b09d7a3e7eae0af3e47c6f7cc4" + integrity sha512-2Pm0Z6v9IfqYhZciYW9i3ZUqOkLIf/AO2Ll389G0LlHJ/qg82sFhL0V4wUI1JQE6nd4eLBiUwhdPEPlHPQIWjQ== + dependencies: + commander "2.16.0" + inherits "2.0.3" + intel "1.2.0" + lodash "4.17.10" + semver "5.5.0" + strip-json-comments "2.0.1" + +postman-collection@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-3.1.1.tgz#9042c1e7891f3f319566fd05f6f2aeeb51bc8d45" + integrity sha512-0Q9BpVVdquv4Wf/Kpvf8LgLADsnZW8g4lGouBncD2pn+mHzL72oWJmD9/kV56wp4SuQl0a1OZNuUYkK9fYPxOA== + dependencies: + escape-html "1.0.3" + file-type "3.9.0" + http-reasons "0.1.0" + iconv-lite "0.4.22" + liquid-json "0.3.1" + lodash "4.17.10" + marked "0.4.0" + mime-format "2.0.0" + mime-types "2.1.18" + postman-url-encoder "1.0.1" + sanitize-html "1.18.2" + semver "5.5.0" + uuid "3.3.2" + +postman-request@2.86.1-postman.1: + version "2.86.1-postman.1" + resolved "https://registry.yarnpkg.com/postman-request/-/postman-request-2.86.1-postman.1.tgz#bc43b753771e8fdcbad95f1436881f81e6c5bef2" + integrity sha512-HzzRbCLcOItaFhhvYiv0/LWShEZ4Lir8ZCL2OiQ8pkpirKM9u7BUQ4OgqNzTExt3m8NWg60f19eQ0hk1cNphLg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + postman-url-encoder "1.0.1" + qs "~6.5.1" + safe-buffer "^5.1.1" + stream-length "^1.0.2" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +postman-runtime@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/postman-runtime/-/postman-runtime-7.2.0.tgz#9d7796fd6981826b8abb887a02370059a02a04e2" + integrity sha512-penzRSjXckHeGXMP6NxvJVLbhxDa47Uei8RIbzf4gEV+1qTZ5qp9QppW2yWPNb5SSW1Z113t6LGKlpVR+plZMQ== + dependencies: + async "2.6.1" + aws4 "1.7.0" + btoa "1.2.1" + crypto-js "3.1.9-1" + eventemitter3 "3.1.0" + hawk "6.0.2" + http-reasons "0.1.0" + httpntlm "1.7.6" + inherits "2.0.3" + lodash "4.17.10" + node-oauth1 "1.2.2" + postman-collection "3.1.1" + postman-request "2.86.1-postman.1" + postman-sandbox "3.1.1" + resolve-from "4.0.0" + serialised-error "1.1.3" + uuid "3.3.2" + +postman-sandbox@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postman-sandbox/-/postman-sandbox-3.1.1.tgz#31ed0a97e9a2c803166a2080fe879a3377470e0f" + integrity sha512-bch46g1LfPnCeCTYQXKlYDmrnTljAPS74a12z5XCS2lJ4veIitX8y4b+mBZSxzMZ05tIZrUTDv+XoyZbRlpagw== + dependencies: + inherits "2.0.3" + lodash "4.17.10" + uuid "3.3.2" + uvm "1.7.3" + +postman-url-encoder@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postman-url-encoder/-/postman-url-encoder-1.0.1.tgz#a094a42e9415ff0bbfdce0eaa8e6011d449ee83c" + integrity sha1-oJSkLpQV/wu/3ODqqOYBHUSe6Dw= + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +pretty-ms@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-3.2.0.tgz#87a8feaf27fc18414d75441467d411d6e6098a25" + integrity sha512-ZypexbfVUGTFxb0v+m1bUyy92DHe5SyYlnyY0msyms5zd3RwyvNgyxZZsXXgoyzlxjx5MiqtXUdhUfvQbe0A2Q== + dependencies: + parse-ms "^1.0.0" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.0.10.tgz#0d40a82f801fc355567d2ecb65efe3f077f121c5" + integrity sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU= + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.0.5" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +qs@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607" + integrity sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc= + +qs@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.1.0.tgz#ec1d1626b24278d99f0fdf4549e524e24eceeb26" + integrity sha1-7B0WJrJCeNmfD99FSeUk4k7O6yY= + +qs@~6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.0.4.tgz#51019d84720c939b82737e84556a782338ecea7b" + integrity sha1-UQGdhHIMk5uCc36EVWp4Izjs6ns= + +qs@~6.5.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +range-parser@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.0.3.tgz#6872823535c692e2c2a0103826afd82c2e0ff175" + integrity sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU= + +raw-body@~2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" + integrity sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q= + dependencies: + bytes "2.4.0" + iconv-lite "0.4.13" + unpipe "1.0.0" + +rc@^1.0.1, rc@^1.1.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.2, readable-stream@^2.3.5: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + 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" + +readable-stream@^3.1.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== + +registry-auth-token@^3.0.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" + integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= + dependencies: + rc "^1.0.1" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.5.2, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request@2.69.0: + version "2.69.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.69.0.tgz#cf91d2e000752b1217155c005241911991a2346a" + integrity sha1-z5HS4AB1KxIXFVwAUkGRGZGiNGo= + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + bl "~1.0.0" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~1.0.0-rc3" + har-validator "~2.0.6" + hawk "~3.1.0" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.0" + qs "~6.0.2" + stringstream "~0.0.4" + tough-cookie "~2.2.0" + tunnel-agent "~0.4.1" + +require-at@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a" + integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g== + +resolve-from@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.1.6: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= + dependencies: + align-text "^0.1.1" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sanitize-html@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.18.2.tgz#61877ba5a910327e42880a28803c2fbafa8e4642" + integrity sha512-52ThA+Z7h6BnvpSVbURwChl10XZrps5q7ytjTwWcIe9bmJwnVP6cpEVK2NvDOUhGupoqAvNbUz3cpnJDp4+/pg== + dependencies: + chalk "^2.3.0" + htmlparser2 "^3.9.0" + lodash.clonedeep "^4.5.0" + lodash.escaperegexp "^4.1.2" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.mergewith "^4.6.0" + postcss "^6.0.14" + srcset "^1.0.0" + xtend "^4.0.0" + +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= + dependencies: + semver "^5.0.3" + +semver@5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== + +semver@^5.0.3, semver@^5.1.0, semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +send@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.13.1.tgz#a30d5f4c82c8a9bae9ad00a1d9b1bdbe6f199ed7" + integrity sha1-ow1fTILIqbrprQCh2bG9vm8Zntc= + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.3.1" + mime "1.3.4" + ms "0.7.1" + on-finished "~2.3.0" + range-parser "~1.0.3" + statuses "~1.2.1" + +send@0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.13.2.tgz#765e7607c8055452bba6f0b052595350986036de" + integrity sha1-dl52B8gFVFK7pvCwUllTUJhgNt4= + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.3.1" + mime "1.3.4" + ms "0.7.1" + on-finished "~2.3.0" + range-parser "~1.0.3" + statuses "~1.2.1" + +serialised-error@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/serialised-error/-/serialised-error-1.1.3.tgz#8a4c466b29c26ff11016eaf1b5fa2b87ca4cd8b5" + integrity sha512-vybp3GItaR1ZtO2nxZZo8eOo7fnVaNtP3XE2vJKgzkKR2bagCkdJ1EpYYhEMd3qu/80DwQk9KjsNSxE3fXWq0g== + dependencies: + object-hash "^1.1.2" + stack-trace "0.0.9" + uuid "^3.0.0" + +serve-static@~1.10.2: + version "1.10.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.10.3.tgz#ce5a6ecd3101fed5ec09827dac22a9c29bfb0535" + integrity sha1-zlpuzTEB/tXsCYJ9rCKpwpv7BTU= + dependencies: + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.13.2" + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shelljs@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" + integrity sha512-pRXeNrCA2Wd9itwhvLp5LZQvPJ0wU6bcjaTMywHHGX5XWhVN2nzSu7WV0q+oUY7mGK3mgSkDDzP3MgjqdyIgbQ== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +sift@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" + integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.6" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" + integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= + +slug@0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.1.tgz#af08f608a7c11516b61778aa800dce84c518cfda" + integrity sha1-rwj2CKfBFRa2F3iqgA3OhMUYz9o= + dependencies: + unicode ">= 0.3.1" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + 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" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= + dependencies: + hoek "2.x.x" + +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + integrity sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg== + dependencies: + hoek "4.x.x" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + 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" + +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + integrity sha1-66T12pwNyZneaAMti092FzZSA2s= + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.6, source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +srcset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" + integrity sha1-pWad4StC87HV6D7QPHEEb8SPQe8= + dependencies: + array-uniq "^1.0.2" + number-is-nan "^1.0.0" + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + 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" + +stack-trace@0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" + integrity sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU= + +stack-trace@~0.0.9: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +statuses@1, "statuses@>= 1.2.1 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +statuses@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.2.1.tgz#dded45cc18256d51ed40aec142489d5c61026d28" + integrity sha1-3e1FzBglbVHtQK7BQkidXGECbSg= + +stream-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-length/-/stream-length-1.0.2.tgz#8277f3cbee49a4daabcfdb4e2f4a9b5e9f2c9f00" + integrity sha1-gnfzy+5JpNqrz9tOL0qbXp8snwA= + dependencies: + bluebird "^2.6.2" + +strftime@~0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/strftime/-/strftime-0.10.1.tgz#108af1176a7d5252cfbddbdb2af044dfae538389" + integrity sha512-nVvH6JG8KlXFPC0f8lojLgEsPA18lRpLZ+RrJh/NkQV2tqOgZfbas8gcU8SFgnnqR3rWzZPYu6N2A3xzs/8rQg== + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4: + version "0.0.6" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" + integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA== + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-json-comments@2.0.1, strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +symbol@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/symbol/-/symbol-0.3.1.tgz#b6f9a900d496a57f02408f22198c109dda063041" + integrity sha1-tvmpANSWpX8CQI8iGYwQndoGMEE= + +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= + dependencies: + execa "^0.7.0" + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +topo@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" + integrity sha1-6ddRYV0buH3IZdsYL6HKCl71NtU= + dependencies: + hoek "2.x.x" + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tough-cookie@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.2.2.tgz#c83a1830f4e5ef0b93ef2a3488e724f8de016ac7" + integrity sha1-yDoYMPTl7wuT7yo0iOck+N4Basc= + +tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== + dependencies: + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + integrity sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us= + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-is@~1.6.11, type-is@~1.6.6: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +uglify-js@^2.6: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= + +uid-safe@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.0.0.tgz#a7f3c6ca64a1f6a5d04ec0ef3e4c3d5367317137" + integrity sha1-p/PGymSh9qXQTsDvPkw9U2cxcTc= + dependencies: + base64-url "1.2.1" + +undefsafe@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +underscore@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= + +underscore@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" + integrity sha1-a7rwh3UA02vjTsqlhODbn+8DUgk= + +"unicode@>= 0.3.1": + version "13.0.0" + resolved "https://registry.yarnpkg.com/unicode/-/unicode-13.0.0.tgz#0775fe86cdbb1fa30e8d060afe194f71aa0c5306" + integrity sha512-osNPLT4Lqna/sV6DQikrB8m4WxR61/k0fnhfKnkPGcZImczW3IysRXvWxfdqGUjh0Ju2o/tGGgu46mlfc/cpZw== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= + dependencies: + crypto-random-string "^1.0.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= + +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-notifier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" + integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-ci "^1.0.10" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= + dependencies: + prepend-http "^1.0.1" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +utcstring@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/utcstring/-/utcstring-0.1.0.tgz#430fd510ab7fc95b5d5910c902d79880c208436b" + integrity sha1-Qw/VEKt/yVtdWRDJAteYgMIIQ2s= + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + integrity sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg= + +uuid@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== + +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^3.0.0, uuid@^3.1.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uvm@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/uvm/-/uvm-1.7.3.tgz#57b37b218a158fa5c059de8527cd67ab64d82663" + integrity sha512-aKnLDcsr/qSYyiF9p049Kqatk/tHxT/gNanpbDzmdQ+XYo0E8lkCYwf478daiu8rXE3+TznBB8Sw/TKakJ6H1A== + dependencies: + circular-json "0.3.1" + inherits "2.0.3" + lodash "4.17.10" + uuid "3.2.1" + +vary@^1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +vary@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.0.1.tgz#99e4981566a286118dfb2b817357df7993376d10" + integrity sha1-meSYFWaihhGN+yuBc1ffeZM3bRA= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +widest-line@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" + integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== + dependencies: + string-width "^2.1.1" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= + +word-wrap@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.0.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= + +xmlbuilder@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.0.0.tgz#c64e52f8ae097fe5fd46d1c38adaade071ee1b55" + integrity sha512-7RWHlmF1yU/E++BZkRQTEv8ZFAhZ+YHINUAxiZ5LQTKRQq//igpiY8rh7dJqPzgb/IzeC5jH9P7OaCERfM9DwA== + +xtend@^4.0.0, xtend@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" diff --git a/charts/.helmignore b/charts/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/Chart.yaml b/charts/Chart.yaml new file mode 100644 index 0000000..b2beb19 --- /dev/null +++ b/charts/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: app +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/charts/templates/_helpers.yml b/charts/templates/_helpers.yml new file mode 100644 index 0000000..49515f2 --- /dev/null +++ b/charts/templates/_helpers.yml @@ -0,0 +1,24 @@ +{{- define "anythink-tenant.backendHost" -}} + https://{{- .Release.Namespace }}-api. + {{- if eq .Values.clusterEnv "staging" }} + {{- .Values.stagingBackendHost }} + {{- else }} + {{- .Values.productionBackendHost }} + {{- end }} +{{- end }} + +{{- define "anythink-tenant.backendRepository" -}} + {{- if eq .Values.clusterEnv "staging" }} + {{- .Values.backend.image.stagingRepository }} + {{- else }} + {{- .Values.backend.image.repository }} + {{- end }} +{{- end }} + +{{- define "anythink-tenant.frontendRepository" -}} + {{- if eq .Values.clusterEnv "staging" }} + {{- .Values.frontend.image.stagingRepository }} + {{- else }} + {{- .Values.frontend.image.repository }} + {{- end }} +{{- end }} diff --git a/charts/templates/anythink-backend-deployment.yaml b/charts/templates/anythink-backend-deployment.yaml new file mode 100644 index 0000000..260d252 --- /dev/null +++ b/charts/templates/anythink-backend-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{ .Values.backend.serviceName }} + name: {{ .Values.backend.serviceName }} +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + app: {{ .Values.backend.serviceName }} + strategy: + type: Recreate + template: + metadata: + labels: + app: {{ .Values.backend.serviceName }} + date: {{ now | unixEpoch | quote }} + spec: + containers: + - args: + - sh + - -c + - "yarn seeds && yarn start" + env: + - name: MONGODB_URI + value: "{{ .Values.database.connectionProtocol }}{{ .Values.database.serviceName }}:{{ .Values.database.servicePort }}/{{ .Values.database.databaseName }}" + - name: NODE_ENV + value: development + - name: PORT + value: "{{ .Values.backend.containerPort }}" + image: "{{ include "anythink-tenant.backendRepository" .}}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + name: {{ .Values.backend.serviceName }} + ports: + - containerPort: {{ .Values.backend.containerPort }} + name: http + protocol: TCP + startupProbe: + httpGet: + path: /health + port: http + failureThreshold: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + readinessProbe: + httpGet: + path: /health + port: http + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + restartPolicy: Always diff --git a/charts/templates/anythink-backend-service.yaml b/charts/templates/anythink-backend-service.yaml new file mode 100644 index 0000000..21bb516 --- /dev/null +++ b/charts/templates/anythink-backend-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app: {{ .Values.backend.serviceName }} + name: {{ .Values.backend.serviceName }} +spec: + ports: + - name: "{{ .Values.backend.containerPort }}" + port: {{ .Values.backend.containerPort }} + targetPort: {{ .Values.backend.containerPort }} + selector: + app: {{ .Values.backend.serviceName }} diff --git a/charts/templates/anythink-frontend-deployment.yaml b/charts/templates/anythink-frontend-deployment.yaml new file mode 100644 index 0000000..f9be249 --- /dev/null +++ b/charts/templates/anythink-frontend-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{ .Values.frontend.serviceName }} + name: {{ .Values.frontend.serviceName }} +spec: + replicas: {{ .Values.frontend.replicaCount }} + selector: + matchLabels: + app: {{ .Values.frontend.serviceName }} + strategy: + type: Recreate + template: + metadata: + labels: + app: {{ .Values.frontend.serviceName }} + date: {{ now | unixEpoch | quote }} + spec: + containers: + - args: + - sh + - -c + - yarn start + env: + - name: NODE_ENV + value: development + - name: PORT + value: "{{ .Values.frontend.containerPort }}" + - name: REACT_APP_BACKEND_URL + value: {{ include "anythink-tenant.backendHost" .}} + image: "{{ include "anythink-tenant.frontendRepository" .}}:{{ .Values.frontend.image.tag }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + name: {{ .Values.frontend.serviceName }} + ports: + - containerPort: {{ .Values.frontend.containerPort }} + name: http + protocol: TCP + startupProbe: + httpGet: + path: / + port: http + failureThreshold: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + restartPolicy: Always diff --git a/charts/templates/anythink-frontend-service.yaml b/charts/templates/anythink-frontend-service.yaml new file mode 100644 index 0000000..217f8c5 --- /dev/null +++ b/charts/templates/anythink-frontend-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: {{ .Values.frontend.serviceName }} + name: {{ .Values.frontend.serviceName }} +spec: + ports: + - name: "{{ .Values.frontend.containerPort }}" + port: {{ .Values.frontend.containerPort }} + targetPort: {{ .Values.frontend.containerPort }} + selector: + app: {{ .Values.frontend.serviceName }} diff --git a/charts/templates/database-deployment.yaml b/charts/templates/database-deployment.yaml new file mode 100644 index 0000000..62deccc --- /dev/null +++ b/charts/templates/database-deployment.yaml @@ -0,0 +1,35 @@ +{{- if .Values.database.deploy }} +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{ .Values.database.serviceName }} + name: {{ .Values.database.serviceName }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Values.database.serviceName }} + strategy: + type: Recreate + template: + metadata: + labels: + app: {{ .Values.database.serviceName }} + spec: + containers: + - image: "{{ .Values.database.image.repository }}:{{ .Values.database.image.tag }}" + name: {{ .Values.database.serviceName }} + imagePullPolicy: {{ .Values.database.image.pullPolicy }} + ports: + - containerPort: {{ .Values.database.containerPort }} + resources: {} + volumeMounts: + - mountPath: /data/db + name: {{ .Values.database.serviceName }}-0 + restartPolicy: Always + volumes: + - name: {{ .Values.database.serviceName }}-0 + persistentVolumeClaim: + claimName: {{ .Values.database.serviceName }}-0 +{{- end }} diff --git a/charts/templates/database-pvc.yaml b/charts/templates/database-pvc.yaml new file mode 100644 index 0000000..88517f3 --- /dev/null +++ b/charts/templates/database-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: {{ .Values.database.serviceName }}-0 + name: {{ .Values.database.serviceName }}-0 +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Mi diff --git a/charts/templates/database-service.yaml b/charts/templates/database-service.yaml new file mode 100644 index 0000000..80b47d3 --- /dev/null +++ b/charts/templates/database-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: {{ .Values.database.serviceName }} + name: {{ .Values.database.serviceName }} +spec: + ports: + - name: "{{ .Values.database.servicePort }}" + port: {{ .Values.database.servicePort }} + targetPort: {{ .Values.database.servicePort }} + selector: + app: {{ .Values.database.serviceName }} diff --git a/charts/values.yaml b/charts/values.yaml new file mode 100644 index 0000000..1fa785d --- /dev/null +++ b/charts/values.yaml @@ -0,0 +1,68 @@ +clusterEnv: "" +productionBackendHost: "prod.anythink.market" +stagingBackendHost: "staging.anythink.market" + +backend: + serviceName: anythink-backend + containerPort: 3000 + replicaCount: 1 + service: + type: ClusterIP + port: 80 + image: + repository: 498915426792.dkr.ecr.us-east-2.amazonaws.com/anythink-backend + stagingRepository: 498915426792.dkr.ecr.us-east-2.amazonaws.com/staging-anythink-backend + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + resources: + limits: + cpu: 100m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + +frontend: + serviceName: anythink-frontend + containerPort: 3001 + replicaCount: 1 + service: + type: ClusterIP + port: 80 + image: + repository: 498915426792.dkr.ecr.us-east-2.amazonaws.com/anythink-frontend + stagingRepository: 498915426792.dkr.ecr.us-east-2.amazonaws.com/staging-anythink-frontend + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + resources: + limits: + cpu: 600m + memory: 768Mi + requests: + cpu: 100m + memory: 128Mi + +database: + deploy: true + serviceName: mongodb-node + containerPort: 27017 + servicePort: 27017 + connectionProtocol: mongodb:// + databaseName: anythink-market + replicaCount: 1 + service: + type: ClusterIP + port: 80 + image: + repository: mongo + pullPolicy: IfNotPresent + tag: "latest" + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi diff --git a/docker-compose.yml b/docker-compose.yml index ba80a9a..0d2be3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,61 +1,59 @@ -# build command: docker compose build -# docker uses the .env file BY DEFAULT. Any other name and you'll have to specify it in the command line -# docker compose --env-file FILENAME.env build - -# docker compose version -version: '3.4' - -# Services +version: "3.8" services: + anythink-backend-node: + build: ./backend + container_name: anythink-backend-node + command: sh -c "cd backend && yarn install && /wait-for-it.sh mongodb-node:27017 -q -t 60 && yarn dev" - # Nginx Service - nginx: - - image: nginx:alpine - ports: - - 80:80 + environment: + - NODE_ENV=development + - PORT=3000 + - MONGODB_URI=mongodb://mongodb-node:27017/anythink-market + - GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN=${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN} + working_dir: /usr/src volumes: - - ./src:/var/www/htdoc - - ./.docker/nginx/conf.d:/etc/nginx/conf.d + - ./:/usr/src/ + - /usr/src/backend/node_modules + ports: + - "3000:3000" depends_on: - - php - - mysql_service + - "mongodb-node" - # PHP Service - php: - build: - dockerfile: ./.docker/php/Dockerfile - args: - RUNTIME_PHP_IMAGE: php:8.2-fpm - # container-path - working_dir: /var/www/htdoc - command: sh -c "./init_repo.sh && php-fpm" - volumes: - - ./src:/var/www/htdoc + anythink-frontend-react: + build: ./frontend + container_name: anythink-frontend-react + command: sh -c "cd frontend && yarn install && /wait-for-it.sh anythink-backend-node:3000 -t 120 --strict -- curl --head -X GET --retry 30 --retry-connrefused --retry-delay 1 anythink-backend-node:3000/api/ping && yarn start" environment: - - DATABASE_URL=mysql://root:rootpassword@mysql_service:3306/mydatabase + - NODE_ENV=development + - PORT=3001 + - REACT_APP_BACKEND_URL=${CODESPACE_BACKEND_URL:-http://localhost:3000} + - WDS_SOCKET_PORT=${CODESPACE_WDS_SOCKET_PORT:-3001} + working_dir: /usr/src + volumes: + - ./:/usr/src/ + - /usr/src/frontend/node_modules + ports: + - "3001:3001" depends_on: - - mysql_service - - - - # MySQL Service - mysql_service: - image: mysql:5.7 - - environment: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_DATABASE: mydatabase - - -# redis: -# image: redis:latest -# ports: -# - "6379:6379" -# volumes: -# - ${REDIS_DATA_HOST_PATH}:${REDIS_DATA_CONTAINER_PATH} + - "anythink-backend-node" + + mongodb-node: + container_name: mongodb-node + restart: always + image: mongo + logging: + driver: none + volumes: + - ~/mongo/data:/data/db + ports: + - '27017:27017' -# Notes: -# -# From Docker Compose version 3.4 the name of the volume can be dynamically generated from environment variables placed in an .env file (this file has to be in the same folder as docker-compose.yml is). -# + anythink-ack: + build: ./frontend + container_name: anythink-ack + command: sh -c "/wait-for-it.sh anythink-frontend-react:3001 -q -t 1000 && ./anythink_ack.sh" + working_dir: /usr/src + volumes: + - ./:/usr/src/ + depends_on: + - "anythink-frontend-react" diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 0000000..dd87e2d --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1,2 @@ +node_modules +build diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..edfc119 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,16 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules + +# testing +coverage + +# production +build + +# misc +.DS_Store +.env +npm-debug.log +.idea \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..c8ba554 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1 @@ +FROM public.ecr.aws/v0a2l7y2/wilco/anythink-frontend-react:latest diff --git a/frontend/Dockerfile.aws b/frontend/Dockerfile.aws new file mode 100644 index 0000000..9485ac1 --- /dev/null +++ b/frontend/Dockerfile.aws @@ -0,0 +1,9 @@ +FROM node:16 +WORKDIR /usr/src + +COPY frontend ./frontend +COPY .wilco ./.wilco + +# Pre-install npm packages +WORKDIR /usr/src/frontend +RUN yarn install diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 0000000..582fe82 --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,8 @@ +const config = { + verbose: true, + jest: { + setupFilesAfterEnv: ["src/setupTests.js"], + }, +}; + +module.exports = config; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..45a643d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,74 @@ +{ + "name": "anythink-market-front", + "version": "0.1.0", + "engines": { + "node": "^16" + }, + "private": true, + "devDependencies": { + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7", + "core-js": "^3.25.1", + "enzyme": "^3.11.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-react": "^7.26.1", + "prettier": "2.4.1", + "react-test-renderer": "^17.0.2", + "redux-mock-store": "^1.5.4" + }, + "dependencies": { + "@babel/core": "^7.18.13", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-syntax-flow": "^7.18.6", + "@babel/plugin-transform-react-jsx": "^7.18.10", + "bootstrap": "^4.6.0", + "bootstrap-icons": "^1.7.1", + "history": "^4.6.3", + "jquery": "^3.6.1", + "marked": "^0.3.6", + "postcss": "^8.4.16", + "prop-types": "^15.5.10", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-redux": "^5.0.7", + "react-router-dom": "^6.9.0", + "react-scripts": "^5.0.1", + "redux": "^3.6.0", + "redux-devtools-extension": "^2.13.2", + "sass": "^1.45.0", + "superagent": "^3.8.2", + "superagent-promise": "^1.1.0", + "typescript": "^4.8.2" + }, + "scripts": { + "start": "REACT_APP_WILCO_ID=${WILCO_ID:-\"$(cat ../.wilco)\"} react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject", + "format": "yarn prettier --write .", + "lint": "yarn eslint . && yarn prettier --check ." + }, + "eslintConfig": { + "extends": [ + "react-app", + "eslint:recommended" + ], + "rules": { + "no-var": "error" + } + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "resolutions": { + "autoprefixer": "10.4.5" + } +} diff --git a/frontend/public/50precentoff.png b/frontend/public/50precentoff.png new file mode 100644 index 0000000..4c835a1 Binary files /dev/null and b/frontend/public/50precentoff.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..201ae78 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..f0441fc --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + Anythink Market + + +
+
+ + diff --git a/frontend/public/placeholder.png b/frontend/public/placeholder.png new file mode 100644 index 0000000..bf1d310 Binary files /dev/null and b/frontend/public/placeholder.png differ diff --git a/frontend/public/style.css b/frontend/public/style.css new file mode 100644 index 0000000..2bb7fe2 --- /dev/null +++ b/frontend/public/style.css @@ -0,0 +1,54 @@ +* { + -webkit-font-smoothing: antialiased; +} +body { + font-family: "Inter", sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + -webkit-font-feature-settings: "kern" 1, "liga" 1; + font-feature-settings: "kern" 1, "liga" 1; + scroll-behavior: smooth; +} +.top-announcement { + background-color: #59ca00; + padding: 15px; + font-size: 18px; + color: white; +} +.logo-text { + color: #59ca00 !important; + font-weight: 600; +} +.minegeek-navbar { + background-color: #393939; +} +.sunray { + background-image: url("sunray.jpeg"); + background-size: cover; +} +.text-white { + color: white; +} +.row-eq-height { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.minegear-btn { + background-color: #59ca00; + border: 0px; + border-radius: 0px; +} +.dramaticPerson { + opacity: 0; + position: fixed; + right: -500px; + bottom: 0px; + height: 590px; + width: 490px; + z-index: 1041; + background-size: cover; +} diff --git a/frontend/public/sunray.jpeg b/frontend/public/sunray.jpeg new file mode 100644 index 0000000..f133496 Binary files /dev/null and b/frontend/public/sunray.jpeg differ diff --git a/frontend/public/verified_seller.svg b/frontend/public/verified_seller.svg new file mode 100644 index 0000000..2e1b353 --- /dev/null +++ b/frontend/public/verified_seller.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/frontend/readme.md b/frontend/readme.md new file mode 100644 index 0000000..2f5bc17 --- /dev/null +++ b/frontend/readme.md @@ -0,0 +1,26 @@ +# Anythink Frontend + +The Anythink Frontend is an SPA written with [React](https://reactjs.org/) and [Redux](https://redux.js.org/) + +## Getting started + +Make sure your server is up and running to serve requests. + +## Pages overview + +- Home page (URL: /#/ ) + - List of tags + - List of items pulled from either Feed, Global, or by Tag + - Pagination for list of items +- Sign in/Sign up pages (URL: /#/login, /#/register ) + - Use JWT (store the token in localStorage) +- Settings page (URL: /#/settings ) +- Editor page to create/edit articles (URL: /#/editor, /#/editor/slug ) +- Item page (URL: /#/item/slug ) + - Delete item button (only shown to item's author) + - Render markdown from server client side + - Comments section at bottom of page + - Delete comment button (only shown to comment's author) +- Profile page (URL: /#/@username, /#/@username/favorites ) + - Show basic user info + - List of items populated from seller's items or user favorite items diff --git a/frontend/src/agent.js b/frontend/src/agent.js new file mode 100644 index 0000000..972f3e3 --- /dev/null +++ b/frontend/src/agent.js @@ -0,0 +1,98 @@ +import superagentPromise from "superagent-promise"; +import _superagent from "superagent"; + +const superagent = superagentPromise(_superagent, global.Promise); + +const BACKEND_URL = + process.env.NODE_ENV !== "production" + ? process.env.REACT_APP_BACKEND_URL + : "https://api.anythink.market"; + +const API_ROOT = `${BACKEND_URL}/api`; + +const encode = encodeURIComponent; +const responseBody = (res) => res.body; + +let token = null; +const tokenPlugin = (req) => { + if (token) { + req.set("authorization", `Token ${token}`); + } +}; + +const requests = { + del: (url) => + superagent.del(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), + get: (url) => + superagent.get(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), + put: (url, body) => + superagent + .put(`${API_ROOT}${url}`, body) + .use(tokenPlugin) + .then(responseBody), + post: (url, body) => + superagent + .post(`${API_ROOT}${url}`, body) + .use(tokenPlugin) + .then(responseBody), +}; + +const Auth = { + current: () => requests.get("/user"), + login: (email, password) => + requests.post("/users/login", { user: { email, password } }), + register: (username, email, password) => + requests.post("/users", { user: { username, email, password } }), + save: (user) => requests.put("/user", { user }), +}; + +const Tags = { + getAll: () => requests.get("/tags"), +}; + +const limit = (count, p) => `limit=${count}&offset=${p ? p * count : 0}`; +const omitSlug = (item) => Object.assign({}, item, { slug: undefined }); +const Items = { + all: (page) => requests.get(`/items?${limit(1000, page)}`), + bySeller: (seller, page) => + requests.get(`/items?seller=${encode(seller)}&${limit(500, page)}`), + byTag: (tag, page) => + requests.get(`/items?tag=${encode(tag)}&${limit(1000, page)}`), + del: (slug) => requests.del(`/items/${slug}`), + favorite: (slug) => requests.post(`/items/${slug}/favorite`), + favoritedBy: (seller, page) => + requests.get(`/items?favorited=${encode(seller)}&${limit(500, page)}`), + feed: () => requests.get("/items/feed?limit=10&offset=0"), + get: (slug) => requests.get(`/items/${slug}`), + unfavorite: (slug) => requests.del(`/items/${slug}/favorite`), + update: (item) => + requests.put(`/items/${item.slug}`, { item: omitSlug(item) }), + create: (item) => requests.post("/items", { item }), +}; + +const Comments = { + create: (slug, comment) => + requests.post(`/items/${slug}/comments`, { comment }), + delete: (slug, commentId) => + requests.del(`/items/${slug}/comments/${commentId}`), + forItem: (slug) => requests.get(`/items/${slug}/comments`), +}; + +const Profile = { + follow: (username) => requests.post(`/profiles/${username}/follow`), + get: (username) => requests.get(`/profiles/${username}`), + unfollow: (username) => requests.del(`/profiles/${username}/follow`), +}; + +const agentObj = { + Items, + Auth, + Comments, + Profile, + Tags, + setToken: (_token) => { + token = _token; + }, +}; + +export default agentObj; diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js new file mode 100644 index 0000000..8b23812 --- /dev/null +++ b/frontend/src/components/App.js @@ -0,0 +1,81 @@ +import agent from "../agent"; +import Header from "./Header"; +import React, { useEffect } from "react"; +import { connect } from "react-redux"; +import { APP_LOAD, REDIRECT } from "../constants/actionTypes"; +import Item from "./Item"; +import Editor from "./Editor"; +import Home from "./Home"; +import Login from "./Login"; +import Profile from "./Profile"; +import ProfileFavorites from "./ProfileFavorites"; +import Register from "./Register"; +import Settings from "./Settings"; +import { Route, Routes, useNavigate } from "react-router-dom"; + +const mapStateToProps = (state) => { + return { + appLoaded: state.common.appLoaded, + appName: state.common.appName, + currentUser: state.common.currentUser, + redirectTo: state.common.redirectTo, + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + onLoad: (payload, token) => + dispatch({ type: APP_LOAD, payload, token, skipTracking: true }), + onRedirect: () => dispatch({ type: REDIRECT }), +}); + +const App = (props) => { + const { redirectTo, onRedirect, onLoad } = props; + const navigate = useNavigate(); + + useEffect(() => { + if (redirectTo) { + navigate(redirectTo); + onRedirect(); + } + }, [redirectTo, onRedirect, navigate]); + + useEffect(() => { + const token = window.localStorage.getItem("jwt"); + if (token) { + agent.setToken(token); + } + onLoad(token ? agent.Auth.current() : null, token); + }, [onLoad]); + + if (props.appLoaded) { + return ( +
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+ ); + } + return ( +
+
+
+ ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(App); \ No newline at end of file diff --git a/frontend/src/components/Editor.js b/frontend/src/components/Editor.js new file mode 100644 index 0000000..700a401 --- /dev/null +++ b/frontend/src/components/Editor.js @@ -0,0 +1,176 @@ +import ListErrors from "./ListErrors"; +import React from "react"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { + ADD_TAG, + EDITOR_PAGE_LOADED, + REMOVE_TAG, + ITEM_SUBMITTED, + EDITOR_PAGE_UNLOADED, + UPDATE_FIELD_EDITOR, +} from "../constants/actionTypes"; +import { withRouterParams } from "./commons"; + +const mapStateToProps = (state) => ({ + ...state.editor, +}); + +const mapDispatchToProps = (dispatch) => ({ + onAddTag: () => dispatch({ type: ADD_TAG }), + onLoad: (payload) => dispatch({ type: EDITOR_PAGE_LOADED, payload }), + onRemoveTag: (tag) => dispatch({ type: REMOVE_TAG, tag }), + onSubmit: (payload) => dispatch({ type: ITEM_SUBMITTED, payload }), + onUnload: (payload) => dispatch({ type: EDITOR_PAGE_UNLOADED }), + onUpdateField: (key, value) => + dispatch({ type: UPDATE_FIELD_EDITOR, key, value }), +}); + +class Editor extends React.Component { + constructor() { + super(); + + const updateFieldEvent = (key) => (ev) => + this.props.onUpdateField(key, ev.target.value); + this.changeTitle = updateFieldEvent("title"); + this.changeDescription = updateFieldEvent("description"); + this.changeImage = updateFieldEvent("image"); + this.changeTagInput = updateFieldEvent("tagInput"); + + this.watchForEnter = (ev) => { + if (ev.keyCode === 13) { + ev.preventDefault(); + this.props.onAddTag(); + } + }; + + this.removeTagHandler = (tag) => () => { + this.props.onRemoveTag(tag); + }; + + this.submitForm = (ev) => { + ev.preventDefault(); + const item = { + title: this.props.title, + description: this.props.description, + image: this.props.image, + tagList: this.props.tagList, + }; + + const slug = { slug: this.props.itemSlug }; + const promise = this.props.itemSlug + ? agent.Items.update(Object.assign(item, slug)) + : agent.Items.create(item); + + this.props.onSubmit(promise); + }; + } + + componentDidUpdate(prevProps) { + if (this.props.params.slug !== prevProps.params.slug) { + if (this.props.params.slug) { + this.props.onUnload(); + return this.props.onLoad(agent.Items.get(this.props.params.slug)); + } + this.props.onLoad(null); + } + } + + componentDidMount() { + if (this.props.params.slug) { + return this.props.onLoad(agent.Items.get(this.props.params.slug)); + } + this.props.onLoad(null); + } + + componentWillUnmount() { + this.props.onUnload(); + } + + render() { + return ( +
+
+
+
+ + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ {(this.props.tagList || []).map((tag) => { + return ( + + + {tag} + + ); + })} +
+
+ + +
+
+
+
+
+
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(withRouterParams(Editor)); diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js new file mode 100644 index 0000000..be37dfc --- /dev/null +++ b/frontend/src/components/Header.js @@ -0,0 +1,73 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import logo from "../imgs/topbar_logo.png"; + +const LoggedOutView = () => { + return ( + + ); +}; + +const LoggedInView = (props) => { + return ( + + ); +}; + +class Header extends React.Component { + render() { + return ( + + ); + } +} + +export default Header; diff --git a/frontend/src/components/Home/Banner.js b/frontend/src/components/Home/Banner.js new file mode 100644 index 0000000..60eed20 --- /dev/null +++ b/frontend/src/components/Home/Banner.js @@ -0,0 +1,19 @@ +import React from "react"; +import logo from "../../imgs/logo.png"; + +const Banner = () => { + return ( +
+
+ +
+ A place to + get + the cool stuff. +
+
+
+ ); +}; + +export default Banner; diff --git a/frontend/src/components/Home/MainView.js b/frontend/src/components/Home/MainView.js new file mode 100644 index 0000000..bf92549 --- /dev/null +++ b/frontend/src/components/Home/MainView.js @@ -0,0 +1,100 @@ +import ItemList from "../ItemList"; +import React from "react"; +import agent from "../../agent"; +import { connect } from "react-redux"; +import { CHANGE_TAB } from "../../constants/actionTypes"; + +const YourFeedTab = (props) => { + if (props.token) { + const clickHandler = (ev) => { + ev.preventDefault(); + props.onTabClick("feed", agent.Items.feed, agent.Items.feed()); + }; + + return ( +
  • + +
  • + ); + } + return null; +}; + +const GlobalFeedTab = (props) => { + const clickHandler = (ev) => { + ev.preventDefault(); + props.onTabClick("all", agent.Items.all, agent.Items.all()); + }; + return ( +
  • + +
  • + ); +}; + +const TagFilterTab = (props) => { + if (!props.tag) { + return null; + } + + return ( +
  • + +
  • + ); +}; + +const mapStateToProps = (state) => ({ + ...state.itemList, + tags: state.home.tags, + token: state.common.token, +}); + +const mapDispatchToProps = (dispatch) => ({ + onTabClick: (tab, pager, payload) => + dispatch({ type: CHANGE_TAB, tab, pager, payload }), +}); + +const MainView = (props) => { + return ( +
    +
    +
      + + + + + +
    +
    + + +
    + ); +}; + +export default connect(mapStateToProps, mapDispatchToProps)(MainView); diff --git a/frontend/src/components/Home/Tags.js b/frontend/src/components/Home/Tags.js new file mode 100644 index 0000000..01cba5d --- /dev/null +++ b/frontend/src/components/Home/Tags.js @@ -0,0 +1,40 @@ +import React from "react"; +import agent from "../../agent"; + +const Tags = (props) => { + const tags = props.tags; + if (tags) { + return ( +
    + Popular tags: + + {tags.map((tag) => { + const handleClick = (ev) => { + ev.preventDefault(); + props.onClickTag( + tag, + (page) => agent.Items.byTag(tag, page), + agent.Items.byTag(tag) + ); + }; + + return ( + + ); + })} + +
    + ); + } else { + return
    Loading Tags...
    ; + } +}; + +export default Tags; diff --git a/frontend/src/components/Home/index.js b/frontend/src/components/Home/index.js new file mode 100644 index 0000000..34e09ae --- /dev/null +++ b/frontend/src/components/Home/index.js @@ -0,0 +1,54 @@ +import Banner from "./Banner"; +import MainView from "./MainView"; +import React, { useEffect } from "react"; +import Tags from "./Tags"; +import agent from "../../agent"; +import { connect } from "react-redux"; +import { + HOME_PAGE_LOADED, + HOME_PAGE_UNLOADED, + APPLY_TAG_FILTER, +} from "../../constants/actionTypes"; + +const Promise = global.Promise; + +const mapStateToProps = (state) => ({ + ...state.home, + appName: state.common.appName, + token: state.common.token, +}); + +const mapDispatchToProps = (dispatch) => ({ + onClickTag: (tag, pager, payload) => + dispatch({ type: APPLY_TAG_FILTER, tag, pager, payload }), + onLoad: (tab, pager, payload) => + dispatch({ type: HOME_PAGE_LOADED, tab, pager, payload }), + onUnload: () => dispatch({ type: HOME_PAGE_UNLOADED }), +}); + +const Home = ({onLoad, onUnload, tags, onClickTag}) => { + const tab = "all"; + const itemsPromise = agent.Items.all; + + useEffect(() => { + onLoad( + tab, + itemsPromise, + Promise.all([agent.Tags.getAll(), itemsPromise()]) + ); + return onUnload; + }, [onLoad, onUnload, tab, itemsPromise]); + + return ( +
    + + +
    + + +
    +
    + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(Home); \ No newline at end of file diff --git a/frontend/src/components/Item/Comment.js b/frontend/src/components/Item/Comment.js new file mode 100644 index 0000000..f852408 --- /dev/null +++ b/frontend/src/components/Item/Comment.js @@ -0,0 +1,42 @@ +import DeleteButton from "./DeleteButton"; +import { Link } from "react-router-dom"; +import React from "react"; + +const Comment = (props) => { + const comment = props.comment; + const show = + props.currentUser && props.currentUser.username === comment.seller.username; + return ( +
    +
    +
    +

    {comment.body}

    +
    + + {comment.seller.username} + +   + + {comment.seller.username} + + | + + {new Date(comment.createdAt).toDateString()} + + +
    +
    +
    +
    + ); +}; + +export default Comment; diff --git a/frontend/src/components/Item/CommentContainer.js b/frontend/src/components/Item/CommentContainer.js new file mode 100644 index 0000000..88562b4 --- /dev/null +++ b/frontend/src/components/Item/CommentContainer.js @@ -0,0 +1,46 @@ +import CommentInput from "./CommentInput"; +import CommentList from "./CommentList"; +import { Link } from "react-router-dom"; +import React from "react"; + +const CommentContainer = (props) => { + if (props.currentUser) { + return ( +
    + +
    +
    + + +
    +
    +
    + ); + } else { + return ( +
    + +

    + + Sign in + +  or  + + sign up + +  to add comments on this item. +

    +
    + ); + } +}; + +export default CommentContainer; diff --git a/frontend/src/components/Item/CommentInput.js b/frontend/src/components/Item/CommentInput.js new file mode 100644 index 0000000..250a241 --- /dev/null +++ b/frontend/src/components/Item/CommentInput.js @@ -0,0 +1,59 @@ +import React from "react"; +import agent from "../../agent"; +import { connect } from "react-redux"; +import { ADD_COMMENT } from "../../constants/actionTypes"; + +const mapDispatchToProps = (dispatch) => ({ + onSubmit: (payload) => dispatch({ type: ADD_COMMENT, payload }), +}); + +class CommentInput extends React.Component { + constructor() { + super(); + this.state = { + body: "", + }; + + this.setBody = (ev) => { + this.setState({ body: ev.target.value }); + }; + + this.createComment = async (ev) => { + ev.preventDefault(); + agent.Comments.create(this.props.slug, { + body: this.state.body, + }).then((payload) => { + this.props.onSubmit(payload); + }); + this.setState({ body: "" }); + }; + } + + render() { + return ( +
    +
    + +
    +
    + {this.props.currentUser.username} + +
    +
    + ); + } +} + +export default connect(() => ({}), mapDispatchToProps)(CommentInput); diff --git a/frontend/src/components/Item/CommentList.js b/frontend/src/components/Item/CommentList.js new file mode 100644 index 0000000..b1bcb35 --- /dev/null +++ b/frontend/src/components/Item/CommentList.js @@ -0,0 +1,21 @@ +import Comment from "./Comment"; +import React from "react"; + +const CommentList = (props) => { + return ( +
    + {props.comments.map((comment) => { + return ( + + ); + })} +
    + ); +}; + +export default CommentList; diff --git a/frontend/src/components/Item/DeleteButton.js b/frontend/src/components/Item/DeleteButton.js new file mode 100644 index 0000000..b78b1b2 --- /dev/null +++ b/frontend/src/components/Item/DeleteButton.js @@ -0,0 +1,27 @@ +import React from "react"; +import agent from "../../agent"; +import { connect } from "react-redux"; +import { DELETE_COMMENT } from "../../constants/actionTypes"; + +const mapDispatchToProps = (dispatch) => ({ + onClick: (payload, commentId) => + dispatch({ type: DELETE_COMMENT, payload, commentId }), +}); + +const DeleteButton = (props) => { + const del = () => { + const payload = agent.Comments.delete(props.slug, props.commentId); + props.onClick(payload, props.commentId); + }; + + if (props.show) { + return ( + + + + ); + } + return null; +}; + +export default connect(() => ({}), mapDispatchToProps)(DeleteButton); diff --git a/frontend/src/components/Item/ItemActions.js b/frontend/src/components/Item/ItemActions.js new file mode 100644 index 0000000..b6d8615 --- /dev/null +++ b/frontend/src/components/Item/ItemActions.js @@ -0,0 +1,36 @@ +import { Link } from "react-router-dom"; +import React from "react"; +import agent from "../../agent"; +import { connect } from "react-redux"; +import { DELETE_ITEM } from "../../constants/actionTypes"; + +const mapDispatchToProps = (dispatch) => ({ + onClickDelete: (payload) => dispatch({ type: DELETE_ITEM, payload }), +}); + +const ItemActions = (props) => { + const item = props.item; + const del = () => { + props.onClickDelete(agent.Items.del(item.slug)); + }; + if (props.canModify) { + return ( + + + Edit Item + + + + + ); + } + + return ; +}; + +export default connect(() => ({}), mapDispatchToProps)(ItemActions); diff --git a/frontend/src/components/Item/ItemMeta.js b/frontend/src/components/Item/ItemMeta.js new file mode 100644 index 0000000..c3bdb6e --- /dev/null +++ b/frontend/src/components/Item/ItemMeta.js @@ -0,0 +1,30 @@ +import ItemActions from "./ItemActions"; +import { Link } from "react-router-dom"; +import React from "react"; + +const ItemMeta = (props) => { + const item = props.item; + return ( +
    + + {item.seller.username} + + +
    + + {item.seller.username} + + {new Date(item.createdAt).toDateString()} +
    + + +
    + ); +}; + +export default ItemMeta; diff --git a/frontend/src/components/Item/index.js b/frontend/src/components/Item/index.js new file mode 100644 index 0000000..7d1bf58 --- /dev/null +++ b/frontend/src/components/Item/index.js @@ -0,0 +1,85 @@ +import ItemMeta from "./ItemMeta"; +import CommentContainer from "./CommentContainer"; +import React, { useEffect } from "react"; +import { connect } from "react-redux"; +import marked from "marked"; +import { + ITEM_PAGE_LOADED, + ITEM_PAGE_UNLOADED, +} from "../../constants/actionTypes"; +import { getItemAndComments } from "./utils/ItemFetcher"; +import { useParams } from "react-router-dom"; + +const mapStateToProps = (state) => ({ + ...state.item, + currentUser: state.common.currentUser, +}); + +const mapDispatchToProps = (dispatch) => ({ + onLoad: (payload) => dispatch({ type: ITEM_PAGE_LOADED, payload }), + onUnload: () => dispatch({ type: ITEM_PAGE_UNLOADED }), +}); + +const Item = (props) => { + const params = useParams(); + const {onLoad, onUnload} = props; + useEffect(() => { + getItemAndComments( + params.id + ).then(([item, comments]) => { + onLoad([item, comments]); + }); + return onUnload; + }, [onLoad, onUnload, params]); + + if (!props.item) { + return null; + } + + const markup = { + __html: marked(props.item.description, { sanitize: true }), + }; + const canModify = + props.currentUser && + props.currentUser.username === props.item.seller.username; + return ( +
    +
    +
    +
    + {props.item.title} +
    + +
    +

    {props.item.title}

    + +
    + {props.item.tagList.map((tag) => { + return ( + + {tag} + + ); + })} +
    +
    + +
    + +
    +
    +
    + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(Item); diff --git a/frontend/src/components/Item/utils/ItemFetcher.js b/frontend/src/components/Item/utils/ItemFetcher.js new file mode 100644 index 0000000..6ebd6c8 --- /dev/null +++ b/frontend/src/components/Item/utils/ItemFetcher.js @@ -0,0 +1,8 @@ +import agent from "../../../agent"; + +export async function getItemAndComments(id) { + const item = await agent.Items.get(id); + const comments = await agent.Comments.forItem(id); + + return [item, comments]; +} diff --git a/frontend/src/components/ItemList.js b/frontend/src/components/ItemList.js new file mode 100644 index 0000000..268714e --- /dev/null +++ b/frontend/src/components/ItemList.js @@ -0,0 +1,35 @@ +import ItemPreview from "./ItemPreview"; +import ListPagination from "./ListPagination"; +import React from "react"; + +const ItemList = (props) => { + if (!props.items) { + return
    Loading...
    ; + } + + if (props.items.length === 0) { + return
    No items are here... yet.
    ; + } + + return ( +
    +
    + {props.items.map((item) => { + return ( +
    + +
    + ); + })} +
    + + +
    + ); +}; + +export default ItemList; diff --git a/frontend/src/components/ItemPreview.js b/frontend/src/components/ItemPreview.js new file mode 100644 index 0000000..ce89b00 --- /dev/null +++ b/frontend/src/components/ItemPreview.js @@ -0,0 +1,66 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { ITEM_FAVORITED, ITEM_UNFAVORITED } from "../constants/actionTypes"; + +const mapDispatchToProps = (dispatch) => ({ + favorite: (slug) => + dispatch({ + type: ITEM_FAVORITED, + payload: agent.Items.favorite(slug), + }), + unfavorite: (slug) => + dispatch({ + type: ITEM_UNFAVORITED, + payload: agent.Items.unfavorite(slug), + }), +}); + +const ItemPreview = (props) => { + const item = props.item; + + const handleClick = (ev) => { + ev.preventDefault(); + if (item.favorited) { + props.unfavorite(item.slug); + } else { + props.favorite(item.slug); + } + }; + + return ( +
    + item +
    + +

    {item.title}

    +

    {item.description}

    + +
    + + {item.seller.username} + + +
    +
    +
    + ); +}; + +export default connect(() => ({}), mapDispatchToProps)(ItemPreview); diff --git a/frontend/src/components/ListErrors.js b/frontend/src/components/ListErrors.js new file mode 100644 index 0000000..33c97e8 --- /dev/null +++ b/frontend/src/components/ListErrors.js @@ -0,0 +1,24 @@ +import React from "react"; + +class ListErrors extends React.Component { + render() { + const errors = this.props.errors; + if (errors) { + return ( + + ); + } else { + return null; + } + } +} + +export default ListErrors; diff --git a/frontend/src/components/ListPagination.js b/frontend/src/components/ListPagination.js new file mode 100644 index 0000000..fcefbcc --- /dev/null +++ b/frontend/src/components/ListPagination.js @@ -0,0 +1,52 @@ +import React from "react"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { SET_PAGE } from "../constants/actionTypes"; + +const mapDispatchToProps = (dispatch) => ({ + onSetPage: (page, payload) => dispatch({ type: SET_PAGE, page, payload }), +}); + +const ListPagination = (props) => { + if (props.itemsCount <= 10) { + return null; + } + + const range = []; + for (let i = 0; i < Math.ceil(props.itemsCount / 10); ++i) { + range.push(i); + } + + const setPage = (page) => { + if (props.pager) { + props.onSetPage(page, props.pager(page)); + } else { + props.onSetPage(page, agent.Items.all(page)); + } + }; + + return ( + + ); +}; + +export default connect(() => ({}), mapDispatchToProps)(ListPagination); diff --git a/frontend/src/components/Login.js b/frontend/src/components/Login.js new file mode 100644 index 0000000..af4f12d --- /dev/null +++ b/frontend/src/components/Login.js @@ -0,0 +1,121 @@ +import { Link } from "react-router-dom"; +import ListErrors from "./ListErrors"; +import React from "react"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { + UPDATE_FIELD_AUTH, + LOGIN, + LOGIN_PAGE_UNLOADED, +} from "../constants/actionTypes"; + +const mapStateToProps = (state) => ({ ...state.auth }); + +const mapDispatchToProps = (dispatch) => ({ + onChangeEmail: (value) => + dispatch({ type: UPDATE_FIELD_AUTH, key: "email", value }), + onChangePassword: (value) => + dispatch({ type: UPDATE_FIELD_AUTH, key: "password", value }), + onSubmit: (email, password) => + dispatch({ type: LOGIN, payload: agent.Auth.login(email, password) }), + onUnload: () => dispatch({ type: LOGIN_PAGE_UNLOADED }), +}); + +class Login extends React.Component { + constructor() { + super(); + this.changeEmail = (ev) => this.props.onChangeEmail(ev.target.value); + this.changePassword = (ev) => this.props.onChangePassword(ev.target.value); + this.submitForm = (email, password) => (ev) => { + ev.preventDefault(); + this.props.onSubmit(email, password); + }; + } + + componentWillUnmount() { + this.props.onUnload(); + } + + render() { + const email = this.props.email; + const password = this.props.password; + return ( +
    +
    +
    +
    +

    Sign In

    + + + +
    +
    +
    +
    +
    + + + +
    + +
    +
    + +
    +
    +
    + + + +
    + +
    +
    + + +
    +
    +

    + + Need an account? + +

    +
    +
    +
    +
    + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Login); diff --git a/frontend/src/components/Profile.js b/frontend/src/components/Profile.js new file mode 100644 index 0000000..b2e0f0b --- /dev/null +++ b/frontend/src/components/Profile.js @@ -0,0 +1,172 @@ +import ItemList from "./ItemList"; +import React from "react"; +import { Link } from "react-router-dom"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { + FOLLOW_USER, + UNFOLLOW_USER, + PROFILE_PAGE_LOADED, + PROFILE_PAGE_UNLOADED, +} from "../constants/actionTypes"; +import { withRouterParams } from "./commons"; + +const EditProfileSettings = (props) => { + if (props.isUser) { + return ( + + Edit Profile Settings + + ); + } + return null; +}; + +const FollowUserButton = (props) => { + if (props.isUser) { + return null; + } + + let classes = "btn btn-sm action-btn"; + if (props.user.following) { + classes += " btn-secondary"; + } else { + classes += " btn-outline-secondary"; + } + + const handleClick = (ev) => { + ev.preventDefault(); + if (props.user.following) { + props.unfollow(props.user.username); + } else { + props.follow(props.user.username); + } + }; + + return ( + + ); +}; + +const mapStateToProps = (state) => ({ + ...state.itemList, + currentUser: state.common.currentUser, + profile: state.profile, +}); + +const mapDispatchToProps = (dispatch) => ({ + onFollow: (username) => + dispatch({ + type: FOLLOW_USER, + payload: agent.Profile.follow(username), + }), + onLoad: (payload) => dispatch({ type: PROFILE_PAGE_LOADED, payload }), + onUnfollow: (username) => + dispatch({ + type: UNFOLLOW_USER, + payload: agent.Profile.unfollow(username), + }), + onUnload: () => dispatch({ type: PROFILE_PAGE_UNLOADED }), +}); + +class Profile extends React.Component { + componentDidMount() { + const username = this.props.params.username?.substring(1); + this.props.onLoad( + Promise.all([ + agent.Profile.get(username), + agent.Items.bySeller(username), + ]) + ); + } + + componentWillUnmount() { + this.props.onUnload(); + } + + renderTabs() { + return ( + + ); + } + + render() { + const profile = this.props.profile; + if (!profile) { + return null; + } + + const isUser = + this.props.currentUser && + this.props.profile.username === this.props.currentUser.username; + + return ( +
    +
    +
    +
    + {profile.username} +

    {profile.username}

    +

    {profile.bio}

    + + + +
    +
    +
    + +
    +
    +
    +
    {this.renderTabs()}
    + + +
    +
    +
    +
    + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(withRouterParams(Profile)); +export { Profile, mapStateToProps }; diff --git a/frontend/src/components/ProfileFavorites.js b/frontend/src/components/ProfileFavorites.js new file mode 100644 index 0000000..5fd3fba --- /dev/null +++ b/frontend/src/components/ProfileFavorites.js @@ -0,0 +1,56 @@ +import { Profile, mapStateToProps } from "./Profile"; +import React from "react"; +import { Link } from "react-router-dom"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { + PROFILE_PAGE_LOADED, + PROFILE_PAGE_UNLOADED, +} from "../constants/actionTypes"; +import { withRouterParams } from "./commons"; + +const mapDispatchToProps = (dispatch) => ({ + onLoad: (pager, payload) => + dispatch({ type: PROFILE_PAGE_LOADED, pager, payload }), + onUnload: () => dispatch({ type: PROFILE_PAGE_UNLOADED }), +}); + +class ProfileFavorites extends Profile { + componentDidMount() { + const username = this.props.params.username?.substring(1); + this.props.onLoad( + (page) => agent.Items.favoritedBy(username, page), + Promise.all([ + agent.Profile.get(username), + agent.Items.favoritedBy(username), + ]) + ); + } + + componentWillUnmount() { + this.props.onUnload(); + } + + renderTabs() { + return ( + + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(withRouterParams(ProfileFavorites)); diff --git a/frontend/src/components/Register.js b/frontend/src/components/Register.js new file mode 100644 index 0000000..195a25a --- /dev/null +++ b/frontend/src/components/Register.js @@ -0,0 +1,148 @@ +import { Link } from "react-router-dom"; +import ListErrors from "./ListErrors"; +import React from "react"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { + UPDATE_FIELD_AUTH, + REGISTER, + REGISTER_PAGE_UNLOADED, +} from "../constants/actionTypes"; + +const mapStateToProps = (state) => ({ ...state.auth }); + +const mapDispatchToProps = (dispatch) => ({ + onChangeEmail: (value) => + dispatch({ type: UPDATE_FIELD_AUTH, key: "email", value }), + onChangePassword: (value) => + dispatch({ type: UPDATE_FIELD_AUTH, key: "password", value }), + onChangeUsername: (value) => + dispatch({ type: UPDATE_FIELD_AUTH, key: "username", value }), + onSubmit: (username, email, password) => { + const payload = agent.Auth.register(username, email, password); + dispatch({ type: REGISTER, payload }); + }, + onUnload: () => dispatch({ type: REGISTER_PAGE_UNLOADED }), +}); + +class Register extends React.Component { + constructor() { + super(); + this.changeEmail = (ev) => this.props.onChangeEmail(ev.target.value); + this.changePassword = (ev) => this.props.onChangePassword(ev.target.value); + this.changeUsername = (ev) => this.props.onChangeUsername(ev.target.value); + this.submitForm = (username, email, password) => (ev) => { + ev.preventDefault(); + this.props.onSubmit(username, email, password); + }; + } + + componentWillUnmount() { + this.props.onUnload(); + } + + render() { + const email = this.props.email; + const password = this.props.password; + const username = this.props.username; + + return ( +
    +
    +
    +
    +

    Sign Up

    + + + +
    +
    +
    +
    +
    + + + +
    + +
    +
    + +
    +
    +
    + + + +
    + +
    +
    + +
    +
    +
    + + + +
    + +
    +
    + + +
    +
    +

    + + Have an account? + +

    +
    +
    +
    +
    + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Register); diff --git a/frontend/src/components/Settings.js b/frontend/src/components/Settings.js new file mode 100644 index 0000000..e4791bc --- /dev/null +++ b/frontend/src/components/Settings.js @@ -0,0 +1,142 @@ +import ListErrors from "./ListErrors"; +import React, { useCallback, useEffect, useState } from "react"; +import agent from "../agent"; +import { connect } from "react-redux"; +import { + SETTINGS_SAVED, + SETTINGS_PAGE_UNLOADED, + LOGOUT, +} from "../constants/actionTypes"; + +const SettingsForm = ({ currentUser, onSubmitForm }) => { + const [user, setUser] = useState({}); + + useEffect(() => { + if (currentUser) { + setUser(currentUser); + } + }, [currentUser]); + + const updateState = useCallback((field) => (ev) => { + const newState = Object.assign({}, user, { [field]: ev.target.value }); + setUser(newState); + }, [user]); + + const submitForm = useCallback((ev) => { + ev.preventDefault(); + const userToSubmit = { ...user }; + if (!userToSubmit.password) { + delete userToSubmit.password; + } + onSubmitForm(userToSubmit); + }, [user, onSubmitForm]); + + return ( +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + +
    +
    + ); +} + +const mapStateToProps = (state) => ({ + ...state.settings, + currentUser: state.common.currentUser, +}); + +const mapDispatchToProps = (dispatch) => ({ + onClickLogout: () => dispatch({ type: LOGOUT }), + onSubmitForm: (user) => + dispatch({ type: SETTINGS_SAVED, payload: agent.Auth.save(user) }), + onUnload: () => dispatch({ type: SETTINGS_PAGE_UNLOADED }), +}); + +class Settings extends React.Component { + render() { + return ( +
    +
    +
    +
    +

    Your Settings

    + + + + + +
    + + +
    +
    +
    +
    + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Settings); diff --git a/frontend/src/components/commons.js b/frontend/src/components/commons.js new file mode 100644 index 0000000..8f1fd1c --- /dev/null +++ b/frontend/src/components/commons.js @@ -0,0 +1,5 @@ +import { useParams } from "react-router-dom"; + +export function withRouterParams(Component) { + return props => ; +} diff --git a/frontend/src/constants/actionTypes.js b/frontend/src/constants/actionTypes.js new file mode 100644 index 0000000..bb5380b --- /dev/null +++ b/frontend/src/constants/actionTypes.js @@ -0,0 +1,37 @@ +export const APP_LOAD = "APP_LOAD"; +export const REDIRECT = "REDIRECT"; +export const ITEM_SUBMITTED = "ITEM_SUBMITTED"; +export const SETTINGS_SAVED = "SETTINGS_SAVED"; +export const DELETE_ITEM = "DELETE_ITEM"; +export const SETTINGS_PAGE_UNLOADED = "SETTINGS_PAGE_UNLOADED"; +export const HOME_PAGE_LOADED = "HOME_PAGE_LOADED"; +export const HOME_PAGE_UNLOADED = "HOME_PAGE_UNLOADED"; +export const ITEM_PAGE_LOADED = "ITEM_PAGE_LOADED"; +export const ITEM_PAGE_UNLOADED = "ITEM_PAGE_UNLOADED"; +export const ADD_COMMENT = "ADD_COMMENT"; +export const DELETE_COMMENT = "DELETE_COMMENT"; +export const ITEM_FAVORITED = "ITEM_FAVORITED"; +export const ITEM_UNFAVORITED = "ITEM_UNFAVORITED"; +export const SET_PAGE = "SET_PAGE"; +export const APPLY_TAG_FILTER = "APPLY_TAG_FILTER"; +export const CHANGE_TAB = "CHANGE_TAB"; +export const PROFILE_PAGE_LOADED = "PROFILE_PAGE_LOADED"; +export const PROFILE_PAGE_UNLOADED = "PROFILE_PAGE_UNLOADED"; +export const LOGIN = "LOGIN"; +export const LOGOUT = "LOGOUT"; +export const REGISTER = "REGISTER"; +export const LOGIN_PAGE_UNLOADED = "LOGIN_PAGE_UNLOADED"; +export const REGISTER_PAGE_UNLOADED = "REGISTER_PAGE_UNLOADED"; +export const ASYNC_START = "ASYNC_START"; +export const ASYNC_END = "ASYNC_END"; +export const EDITOR_PAGE_LOADED = "EDITOR_PAGE_LOADED"; +export const EDITOR_PAGE_UNLOADED = "EDITOR_PAGE_UNLOADED"; +export const ADD_TAG = "ADD_TAG"; +export const REMOVE_TAG = "REMOVE_TAG"; +export const UPDATE_FIELD_AUTH = "UPDATE_FIELD_AUTH"; +export const UPDATE_FIELD_EDITOR = "UPDATE_FIELD_EDITOR"; +export const FOLLOW_USER = "FOLLOW_USER"; +export const UNFOLLOW_USER = "UNFOLLOW_USER"; +export const PROFILE_FAVORITES_PAGE_UNLOADED = + "PROFILE_FAVORITES_PAGE_UNLOADED"; +export const PROFILE_FAVORITES_PAGE_LOADED = "PROFILE_FAVORITES_PAGE_LOADED"; diff --git a/frontend/src/custom.scss b/frontend/src/custom.scss new file mode 100644 index 0000000..0b4d757 --- /dev/null +++ b/frontend/src/custom.scss @@ -0,0 +1,61 @@ +@import url("https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700,800"); + +// Override default variables before the import +$primary: #2b1456; +$secondary: #ff2b98; + +$body-color: white; //this is the text color +$body-bg: $primary; + +$dark: #170539; +$light: #af93f2; + +$input-border-color: #d0d0d0; + +$font-family-base: "Poppins", sans-serif !important; + +$theme-colors: ( + "light-gray": #f2f2f2, +); + +// Import Bootstrap and its default variables +@import "~bootstrap/scss/bootstrap.scss"; +@import "~bootstrap-icons/font/bootstrap-icons.css"; + +body { + background-image: url("./imgs/background.png"); + background-position: top; + background-repeat: no-repeat; +} +.page { + margin-top: 2 * $spacer; + margin-bottom: 2 * $spacer; +} + +.user-pic { + height: 40px; + width: 40px; +} + +.user-img { + width: 100px; + height: 100px; + border-radius: 100px; +} + +.item-img { + height: 150px; + object-fit: cover; +} + +.crop-text-3 { + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.user-info { + min-width: 800px; +} diff --git a/frontend/src/imgs/background.png b/frontend/src/imgs/background.png new file mode 100644 index 0000000..6c2e0f9 Binary files /dev/null and b/frontend/src/imgs/background.png differ diff --git a/frontend/src/imgs/logo.png b/frontend/src/imgs/logo.png new file mode 100644 index 0000000..89757c2 Binary files /dev/null and b/frontend/src/imgs/logo.png differ diff --git a/frontend/src/imgs/topbar_logo.png b/frontend/src/imgs/topbar_logo.png new file mode 100644 index 0000000..e7fefd3 Binary files /dev/null and b/frontend/src/imgs/topbar_logo.png differ diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 0000000..a6ecd9b --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,18 @@ +import "./custom.scss"; +import ReactDOM from "react-dom"; +import { Provider } from "react-redux"; +import React from "react"; +import { store } from "./store"; + +import App from "./components/App"; +import { BrowserRouter } from "react-router-dom"; + +ReactDOM.render( + + + + + , + + document.getElementById("root") +); diff --git a/frontend/src/middleware.js b/frontend/src/middleware.js new file mode 100644 index 0000000..4f82efb --- /dev/null +++ b/frontend/src/middleware.js @@ -0,0 +1,65 @@ +import agent from "./agent"; +import { + ASYNC_START, + ASYNC_END, + LOGIN, + LOGOUT, + REGISTER, +} from "./constants/actionTypes"; + +const promiseMiddleware = (store) => (next) => (action) => { + if (isPromise(action.payload)) { + store.dispatch({ type: ASYNC_START, subtype: action.type }); + + const currentView = store.getState().viewChangeCounter; + const skipTracking = action.skipTracking; + + action.payload.then( + (res) => { + const currentState = store.getState(); + if (!skipTracking && currentState.viewChangeCounter !== currentView) { + return; + } + action.payload = res; + store.dispatch({ type: ASYNC_END, promise: action.payload }); + store.dispatch(action); + }, + (error) => { + const currentState = store.getState(); + if (!skipTracking && currentState.viewChangeCounter !== currentView) { + return; + } + action.error = true; + action.payload = error.response.body; + if (!action.skipTracking) { + store.dispatch({ type: ASYNC_END, promise: action.payload }); + } + store.dispatch(action); + } + ); + + return; + } + + next(action); +}; + +const localStorageMiddleware = (store) => (next) => (action) => { + if (action.type === REGISTER || action.type === LOGIN) { + if (!action.error) { + window.localStorage.setItem("jwt", action.payload.user.token); + agent.setToken(action.payload.user.token); + } + } else if (action.type === LOGOUT) { + window.localStorage.setItem("jwt", ""); + agent.setToken(null); + } + + next(action); +}; + +function isPromise(v) { + return v && typeof v.then === "function"; +} + +export { promiseMiddleware, localStorageMiddleware }; diff --git a/frontend/src/reducer.js b/frontend/src/reducer.js new file mode 100644 index 0000000..65173ca --- /dev/null +++ b/frontend/src/reducer.js @@ -0,0 +1,20 @@ +import item from "./reducers/item"; +import itemList from "./reducers/itemList"; +import auth from "./reducers/auth"; +import { combineReducers } from "redux"; +import common from "./reducers/common"; +import editor from "./reducers/editor"; +import home from "./reducers/home"; +import profile from "./reducers/profile"; +import settings from "./reducers/settings"; + +export default combineReducers({ + item, + itemList, + auth, + common, + editor, + home, + profile, + settings, +}); diff --git a/frontend/src/reducers/auth.js b/frontend/src/reducers/auth.js new file mode 100644 index 0000000..6128b11 --- /dev/null +++ b/frontend/src/reducers/auth.js @@ -0,0 +1,36 @@ +import { + LOGIN, + REGISTER, + LOGIN_PAGE_UNLOADED, + REGISTER_PAGE_UNLOADED, + ASYNC_START, + UPDATE_FIELD_AUTH, +} from "../constants/actionTypes"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case LOGIN: + case REGISTER: + return { + ...state, + inProgress: false, + errors: action.error ? action.payload.errors : null, + }; + case LOGIN_PAGE_UNLOADED: + case REGISTER_PAGE_UNLOADED: + return {}; + case ASYNC_START: + if (action.subtype === LOGIN || action.subtype === REGISTER) { + return { ...state, inProgress: true }; + } + break; + case UPDATE_FIELD_AUTH: + return { ...state, [action.key]: action.value }; + default: + return state; + } + + return state; +}; + +export default reducer; diff --git a/frontend/src/reducers/common.js b/frontend/src/reducers/common.js new file mode 100644 index 0000000..3ef54b4 --- /dev/null +++ b/frontend/src/reducers/common.js @@ -0,0 +1,79 @@ +import { + APP_LOAD, + REDIRECT, + LOGOUT, + ITEM_SUBMITTED, + SETTINGS_SAVED, + LOGIN, + REGISTER, + DELETE_ITEM, + ITEM_PAGE_UNLOADED, + EDITOR_PAGE_UNLOADED, + HOME_PAGE_UNLOADED, + PROFILE_PAGE_UNLOADED, + PROFILE_FAVORITES_PAGE_UNLOADED, + SETTINGS_PAGE_UNLOADED, + LOGIN_PAGE_UNLOADED, + REGISTER_PAGE_UNLOADED, +} from "../constants/actionTypes"; + +const defaultState = { + appName: "Anythink Market", + token: null, + viewChangeCounter: 0, +}; + +const reducer = (state = defaultState, action) => { + switch (action.type) { + case APP_LOAD: + return { + ...state, + token: action.token || null, + appLoaded: true, + currentUser: action.payload ? action.payload.user : null, + }; + case REDIRECT: + return { ...state, redirectTo: null }; + case LOGOUT: + return { ...state, redirectTo: "/", token: null, currentUser: null }; + case ITEM_SUBMITTED: { + const redirectUrl = `/item/${action.payload.item.slug}`; + return { ...state, redirectTo: redirectUrl }; + } + case SETTINGS_SAVED: + return { + ...state, + redirectTo: action.error ? null : "/", + currentUser: action.error ? null : action.payload.user, + }; + case LOGIN: + return { + ...state, + redirectTo: action.error ? null : "/", + token: action.error ? null : action.payload.user.token, + currentUser: action.error ? null : action.payload.user, + }; + case REGISTER: + return { + ...state, + redirectTo: action.error ? null : `/@${action.payload.user.username}`, + token: action.error ? null : action.payload.user.token, + currentUser: action.error ? null : action.payload.user, + }; + case DELETE_ITEM: + return { ...state, redirectTo: "/" }; + case ITEM_PAGE_UNLOADED: + case EDITOR_PAGE_UNLOADED: + case HOME_PAGE_UNLOADED: + case PROFILE_PAGE_UNLOADED: + case PROFILE_FAVORITES_PAGE_UNLOADED: + case SETTINGS_PAGE_UNLOADED: + case LOGIN_PAGE_UNLOADED: + case REGISTER_PAGE_UNLOADED: + return { ...state, viewChangeCounter: state.viewChangeCounter + 1 }; + default: + return state; + } +}; + +export default reducer; diff --git a/frontend/src/reducers/editor.js b/frontend/src/reducers/editor.js new file mode 100644 index 0000000..4320e70 --- /dev/null +++ b/frontend/src/reducers/editor.js @@ -0,0 +1,56 @@ +import { + EDITOR_PAGE_LOADED, + EDITOR_PAGE_UNLOADED, + ITEM_SUBMITTED, + ASYNC_START, + ADD_TAG, + REMOVE_TAG, + UPDATE_FIELD_EDITOR, +} from "../constants/actionTypes"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case EDITOR_PAGE_LOADED: + return { + ...state, + itemSlug: action.payload ? action.payload.item.slug : "", + title: action.payload ? action.payload.item.title : "", + description: action.payload ? action.payload.item.description : "", + image: action.payload ? action.payload.item.image : "", + tagInput: "", + tagList: action.payload ? action.payload.item.tagList : [], + }; + case EDITOR_PAGE_UNLOADED: + return {}; + case ITEM_SUBMITTED: + return { + ...state, + inProgress: null, + errors: action.error ? action.payload.errors : null, + }; + case ASYNC_START: + if (action.subtype === ITEM_SUBMITTED) { + return { ...state, inProgress: true }; + } + break; + case ADD_TAG: + return { + ...state, + tagList: state.tagList.concat([state.tagInput]), + tagInput: "", + }; + case REMOVE_TAG: + return { + ...state, + tagList: state.tagList.filter((tag) => tag !== action.tag), + }; + case UPDATE_FIELD_EDITOR: + return { ...state, [action.key]: action.value }; + default: + return state; + } + + return state; +}; + +export default reducer; diff --git a/frontend/src/reducers/home.js b/frontend/src/reducers/home.js new file mode 100644 index 0000000..b9bc097 --- /dev/null +++ b/frontend/src/reducers/home.js @@ -0,0 +1,17 @@ +import { HOME_PAGE_LOADED, HOME_PAGE_UNLOADED } from "../constants/actionTypes"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case HOME_PAGE_LOADED: + return { + ...state, + tags: action.payload[0].tags, + }; + case HOME_PAGE_UNLOADED: + return {}; + default: + return state; + } +}; + +export default reducer; diff --git a/frontend/src/reducers/item.js b/frontend/src/reducers/item.js new file mode 100644 index 0000000..918201c --- /dev/null +++ b/frontend/src/reducers/item.js @@ -0,0 +1,38 @@ +import { + ITEM_PAGE_LOADED, + ITEM_PAGE_UNLOADED, + ADD_COMMENT, + DELETE_COMMENT, +} from "../constants/actionTypes"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case ITEM_PAGE_LOADED: + return { + ...state, + item: action.payload[0].item, + comments: action.payload[1].comments, + }; + case ITEM_PAGE_UNLOADED: + return {}; + case ADD_COMMENT: + return { + ...state, + commentErrors: action.error ? action.payload.errors : null, + comments: action.error + ? null + : (state.comments || []).concat([action.payload.comment]), + }; + case DELETE_COMMENT: { + const commentId = action.commentId; + return { + ...state, + comments: state.comments.filter((comment) => comment.id !== commentId), + }; + } + default: + return state; + } +}; + +export default reducer; diff --git a/frontend/src/reducers/itemList.js b/frontend/src/reducers/itemList.js new file mode 100644 index 0000000..016a996 --- /dev/null +++ b/frontend/src/reducers/itemList.js @@ -0,0 +1,88 @@ +import { + ITEM_FAVORITED, + ITEM_UNFAVORITED, + SET_PAGE, + APPLY_TAG_FILTER, + HOME_PAGE_LOADED, + HOME_PAGE_UNLOADED, + CHANGE_TAB, + PROFILE_PAGE_LOADED, + PROFILE_PAGE_UNLOADED, + PROFILE_FAVORITES_PAGE_LOADED, + PROFILE_FAVORITES_PAGE_UNLOADED, +} from "../constants/actionTypes"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case ITEM_FAVORITED: + case ITEM_UNFAVORITED: + return { + ...state, + items: state.items.map((item) => { + if (item.slug === action.payload.item.slug) { + return { + ...item, + favorited: action.payload.item.favorited, + favoritesCount: action.payload.item.favoritesCount, + }; + } + return item; + }), + }; + case SET_PAGE: + return { + ...state, + items: action.payload.items, + itemsCount: action.payload.itemsCount, + currentPage: action.page, + }; + case APPLY_TAG_FILTER: + return { + ...state, + pager: action.pager, + items: action.payload.items, + itemsCount: action.payload.itemsCount, + tab: null, + tag: action.tag, + currentPage: 0, + }; + case HOME_PAGE_LOADED: + return { + ...state, + pager: action.pager, + tags: action.payload[0].tags, + items: action.payload[1].items, + itemsCount: action.payload[1].itemsCount, + currentPage: 0, + tab: action.tab, + }; + case HOME_PAGE_UNLOADED: + return {}; + case CHANGE_TAB: + return { + ...state, + pager: action.pager, + items: action.payload.items, + itemsCount: action.payload.itemsCount, + tab: action.tab, + currentPage: 0, + tag: null, + }; + case PROFILE_PAGE_LOADED: + case PROFILE_FAVORITES_PAGE_LOADED: + return { + ...state, + pager: action.pager, + items: action.payload?.[1]?.items, + itemsCount: action.payload?.[1]?.itemsCount, + currentPage: 0, + }; + case PROFILE_PAGE_UNLOADED: + case PROFILE_FAVORITES_PAGE_UNLOADED: + return {}; + default: + return state; + } +}; + +export default reducer; diff --git a/frontend/src/reducers/profile.js b/frontend/src/reducers/profile.js new file mode 100644 index 0000000..5d4fe85 --- /dev/null +++ b/frontend/src/reducers/profile.js @@ -0,0 +1,26 @@ +import { + PROFILE_PAGE_LOADED, + PROFILE_PAGE_UNLOADED, + FOLLOW_USER, + UNFOLLOW_USER, +} from "../constants/actionTypes"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case PROFILE_PAGE_LOADED: + return { + ...action.payload?.[0]?.profile, + }; + case PROFILE_PAGE_UNLOADED: + return {}; + case FOLLOW_USER: + case UNFOLLOW_USER: + return { + ...action.payload.profile, + }; + default: + return state; + } +}; + +export default reducer; diff --git a/frontend/src/reducers/settings.js b/frontend/src/reducers/settings.js new file mode 100644 index 0000000..2cf4da0 --- /dev/null +++ b/frontend/src/reducers/settings.js @@ -0,0 +1,27 @@ +import { + SETTINGS_SAVED, + SETTINGS_PAGE_UNLOADED, + ASYNC_START, +} from "../constants/actionTypes"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case SETTINGS_SAVED: + return { + ...state, + inProgress: false, + errors: action.error ? action.payload.errors : null, + }; + case SETTINGS_PAGE_UNLOADED: + return {}; + case ASYNC_START: + return { + ...state, + inProgress: true, + }; + default: + return state; + } +}; + +export default reducer; diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js new file mode 100644 index 0000000..0772595 --- /dev/null +++ b/frontend/src/setupTests.js @@ -0,0 +1,5 @@ +import "core-js"; +import { configure } from "enzyme"; +import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; + +configure({ adapter: new Adapter() }); diff --git a/frontend/src/store.js b/frontend/src/store.js new file mode 100644 index 0000000..5ac25dc --- /dev/null +++ b/frontend/src/store.js @@ -0,0 +1,25 @@ +import { applyMiddleware, createStore } from "redux"; +import { composeWithDevTools } from "redux-devtools-extension/developmentOnly"; +import { promiseMiddleware, localStorageMiddleware } from "./middleware"; +import reducer from "./reducer"; + +import { createBrowserHistory } from "history"; + +export const history = createBrowserHistory(); + +const getMiddleware = () => { + if (process.env.NODE_ENV === "production") { + return applyMiddleware( + promiseMiddleware, + localStorageMiddleware + ); + } else { + // Enable additional logging in non-production environments. + return applyMiddleware( + promiseMiddleware, + localStorageMiddleware, + ); + } +}; + +export const store = createStore(reducer, composeWithDevTools(getMiddleware())); diff --git a/frontend/src/tests/components/Header.test.js b/frontend/src/tests/components/Header.test.js new file mode 100644 index 0000000..592b742 --- /dev/null +++ b/frontend/src/tests/components/Header.test.js @@ -0,0 +1,54 @@ +import { create } from "react-test-renderer"; +import { mount } from "enzyme"; +import { BrowserRouter as Router } from "react-router-dom"; +import Header from "../../components/Header"; + +describe("Header component", () => { + it("Snapshot testing with no user", () => { + const component = create( + +
    + + ); + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("Snapshot testing with user", () => { + const component = create( + +
    + + ); + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("Check link to main page", () => { + const header = mount( + +
    + + ); + expect(header.find("Link").first().prop("to")).toEqual("/"); + }); + + it("Render register button when there's no user", () => { + const header = mount( + +
    + + ); + expect(header.find("li > Link").first().text()).toEqual("Sign in"); + }); + + it("Render user name when there's a user", () => { + const user = { username: "user name", image: "image.png" }; + const header = mount( + +
    + + ); + expect(header.find("li > Link").last().text()).toEqual(user.username); + }); +}); diff --git a/frontend/src/tests/components/__snapshots__/Header.test.js.snap b/frontend/src/tests/components/__snapshots__/Header.test.js.snap new file mode 100644 index 0000000..e2509e5 --- /dev/null +++ b/frontend/src/tests/components/__snapshots__/Header.test.js.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header component Snapshot testing with no user 1`] = ` + +`; + +exports[`Header component Snapshot testing with user 1`] = ` + +`; diff --git a/frontend/src/tests/item/CommentInput.test.js b/frontend/src/tests/item/CommentInput.test.js new file mode 100644 index 0000000..ab7fcf5 --- /dev/null +++ b/frontend/src/tests/item/CommentInput.test.js @@ -0,0 +1,64 @@ +import { create } from "react-test-renderer"; +import { mount } from "enzyme"; +import configureMockStore from "redux-mock-store"; +import CommentInput from "../../components/Item/CommentInput"; +import agent from "../../agent"; +import { ADD_COMMENT } from "../../constants/actionTypes"; + +const mockStore = configureMockStore(); +agent.Comments.create = jest.fn(); + +describe("CommentInput component", () => { + let store; + + beforeEach(() => { + store = mockStore({}); + }); + + it("Snapshot testing with no user", () => { + const component = create( + + ); + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("Submit text", () => { + const user = { username: "name", image: "" }; + const component = mount( + + ); + const comment = { id: 1 }; + agent.Comments.create.mockResolvedValue(comment); + + const event = { target: { value: "sometext" } }; + component.find("textarea").simulate("change", event); + component.find("form").simulate("submit"); + + setImmediate(async () => { + expect(store.getActions()).toHaveLength(1); + expect(store.getActions()[0].type).toEqual(ADD_COMMENT); + expect(await store.getActions()[0].payload).toEqual(comment); + }); + }); + + it("Clear text after submit", async () => { + const user = { username: "name", image: "" }; + + const component = mount( + + ); + + const comment = { id: 1 }; + agent.Comments.create.mockResolvedValue(comment); + + const event = { target: { value: "sometext" } }; + component.find("textarea").simulate("change", event); + component.find("form").simulate("submit"); + expect(component.find("textarea").text()).toHaveLength(0); + }); +}); diff --git a/frontend/src/tests/item/__snapshots__/CommentInput.test.js.snap b/frontend/src/tests/item/__snapshots__/CommentInput.test.js.snap new file mode 100644 index 0000000..f739b2c --- /dev/null +++ b/frontend/src/tests/item/__snapshots__/CommentInput.test.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommentInput component Snapshot testing with no user 1`] = ` +
    +
    +