├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── issue_template.md ├── pull_request_template.md └── workflows │ ├── build-publish.yml │ ├── codeql-analysis.yml │ ├── greeting.yml │ ├── lint-test.yml │ └── status-webhook.yml ├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api ├── handlers │ ├── asset_details.go │ ├── create_asset.go │ ├── crud_methods.go │ ├── get_asset.go │ ├── handler.go │ └── hello_world.go ├── handlers_test.go ├── main_test.go ├── middleware.go └── routes.go ├── cache ├── cache_test.go ├── posts-cache.go └── redis-cache.go ├── codecov.yml ├── cover.sh ├── db ├── query │ └── cdn.sql └── sqlc │ ├── cdn.sql.go │ ├── cdn_test.go │ ├── db.go │ ├── main_test.go │ ├── models.go │ └── store.go ├── docker-compose.yml ├── docs ├── docs-template │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── assets │ │ │ ├── arrow.png │ │ │ ├── favicon.png │ │ │ ├── global.css │ │ │ └── link.png │ │ ├── favicon.png │ │ └── index.html │ ├── rollup.config.js │ ├── scripts │ │ └── setupTypeScript.js │ └── src │ │ ├── App.svelte │ │ ├── Docs.svelte │ │ ├── RouteList.svelte │ │ └── main.js └── register.go ├── go.mod ├── go.sum ├── main.go ├── server └── server.go ├── sqlc.yaml ├── test.sh └── utils ├── config.go ├── main_test.go ├── migration.go ├── migration_test.go ├── random.go ├── random_test.go └── responses.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | .git/ 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | *.env 17 | # app.env 18 | .vscode/ 19 | .idea/ 20 | heap.tar.gz 21 | profile.tar.gz 22 | 23 | dependencies 24 | /node_modules 25 | /.pnp 26 | .pnp.js 27 | .yarn/* 28 | !.yarn/releases 29 | !.yarn/plugins 30 | !.yarn/sdks 31 | !.yarn/versions 32 | .pnp.* 33 | 34 | # testing 35 | /coverage 36 | 37 | # production 38 | # /build 39 | 40 | # misc 41 | .DS_Store 42 | .env.local 43 | .env.development.local 44 | .env.test.local 45 | .env.production.local 46 | 47 | npm-debug.log* 48 | yarn-debug.log* 49 | yarn-error.log* 50 | 51 | 52 | .eslintcache 53 | .env 54 | *Dockerfile* -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tech-With-Tim/cdn/741e7483e6858ed5f3f2169a63adcc7f8a9ba2a2/.github/ISSUE_TEMPLATE/config.yml -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tech-With-Tim/cdn/741e7483e6858ed5f3f2169a63adcc7f8a9ba2a2/.github/ISSUE_TEMPLATE/feature_request.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tech-With-Tim/cdn/741e7483e6858ed5f3f2169a63adcc7f8a9ba2a2/.github/ISSUE_TEMPLATE/issue_template.md -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Checklist 6 | 7 | 8 | 9 | - [ ] If endpoints were changed then they have been documented and tested. 10 | - [ ] I have updated the docmentation to reflect the changes. 11 | - [ ] I have updated the tests to support the changes. 12 | - [ ] This PR fixes an issue. 13 | - [ ] This PR adds something new (e.g. new endpoint or parameter). 14 | - [ ] This PR is a breaking change (e.g. endpoint or parameters removed/renamed) 15 | - [ ] This PR is **not** a code change (e.g. documentation, README, ...) 16 | -------------------------------------------------------------------------------- /.github/workflows/build-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Lint & Test"] 6 | branches: ["main"] 7 | types: ["completed"] 8 | 9 | jobs: 10 | build: 11 | if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' 12 | name: Build & Publish 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@v2 # Checking out the repo 18 | 19 | - name: Set Up Submodule 20 | run: | 21 | git submodule init 22 | git submodule update 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v1 26 | 27 | - name: Set up Docker Buildx 28 | id: buildx 29 | uses: docker/setup-buildx-action@v1 30 | 31 | - name: Login to Docker Hub 32 | uses: docker/login-action@v1 33 | with: 34 | username: ${{ secrets.DOCKER_USER }} 35 | password: ${{ secrets.DOCKER_PASS }} 36 | 37 | - name: Cache Docker layers 38 | uses: actions/cache@v2 39 | with: 40 | path: /tmp/.buildx-cache 41 | key: ${{ runner.os }}-buildx-${{ github.sha }} 42 | restore-keys: | 43 | ${{ runner.os }}-buildx- 44 | 45 | - name: Build and Push 46 | id: docker_build 47 | uses: docker/build-push-action@v2 48 | with: 49 | push: true 50 | tags: | 51 | techwithtim/cdn:latest 52 | techwithtim/cdn:${{ github.sha }} 53 | builder: ${{ steps.buildx.outputs.name }} 54 | cache-to: type=local,dest=/tmp/.buildx-cache 55 | cache-from: type=local,src=/tmp/.buildx-cache 56 | 57 | deploy: 58 | name: Deploy on Kubernetes cluster 59 | runs-on: ubuntu-20.04 60 | needs: build 61 | 62 | steps: 63 | - name: Checkout Repo 64 | uses: actions/checkout@v2 65 | with: 66 | repository: Tech-With-Tim/k8s 67 | token: ${{ secrets.REPO_TOKEN }} 68 | 69 | - name: Deploy to Kubernetes 70 | uses: fjogeleit/yaml-update-action@master 71 | with: 72 | repository: Tech-With-Tim/k8s 73 | token: ${{ secrets.REPO_TOKEN }} 74 | branch: "main" 75 | createPR: "false" 76 | updateFile: "true" 77 | message: "Redeploy CDN" 78 | valueFile: "cdn/deployment.yml" 79 | propertyPath: "spec.template.spec.containers.0.image" 80 | value: "techwithtim/cdn:${{ github.sha }}" 81 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: '**' 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: '**' 20 | schedule: 21 | - cron: '30 9 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | - name: Set Up Submodule 45 | run: | 46 | git submodule init 47 | git submodule update 48 | 49 | # Initializes the CodeQL tools for scanning. 50 | - name: Initialize CodeQL 51 | uses: github/codeql-action/init@v1 52 | with: 53 | languages: ${{ matrix.language }} 54 | # If you wish to specify custom queries, you can do so here or in a config file. 55 | # By default, queries listed here will override any specified in a config file. 56 | # Prefix the list here with "+" to use these queries and those in the config file. 57 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v1 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 https://git.io/JvXDl 66 | 67 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 68 | # and modify them (or add more) to build your code if your project 69 | # uses a compiled language 70 | 71 | #- run: | 72 | # make bootstrap 73 | # make release 74 | 75 | - name: Perform CodeQL Analysis 76 | uses: github/codeql-action/analyze@v1 77 | -------------------------------------------------------------------------------- /.github/workflows/greeting.yml: -------------------------------------------------------------------------------- 1 | name: Greeting 2 | 3 | on: [issues, pull_request_target] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/first-interaction@v1 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | issue-message: | 14 | Hi ${{ github.actor }}, Thanks for reporting an issue in our Repository. 15 | If you haven't already, please include relevant information asked for in our templates. 16 | pr-message: | 17 | 18 | Hey **${{ github.actor }}**, welcome to the repo for the Tech With Tim CDN. 19 | Please follow the following guidelines while opening a PR: 20 | 21 | - Any new or changed endpoints should be thoroughly documented. 22 | - Write and or update tests for your new / updated endpoints. 23 | - All code should be easly readable or commented. 24 | 25 | If your code does not meet these requirements your PR will not be accepted. 26 | -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | on: 4 | push: 5 | branches: '**' 6 | pull_request: 7 | branches: '**' 8 | 9 | jobs: 10 | Lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: golangci-lint 16 | uses: golangci/golangci-lint-action@v2 17 | with: 18 | version: latest 19 | args: -e SA1029 20 | Test: 21 | name: Test 22 | runs-on: ubuntu-latest 23 | services: 24 | # Label used to access the service container 25 | postgres: 26 | # Docker Hub image 27 | image: postgres:12 28 | # Provide the password for postgres 29 | env: 30 | POSTGRES_USER: twt 31 | POSTGRES_PASSWORD: twt 32 | POSTGRES_DB: twt 33 | ports: 34 | - 5432:5432 35 | # Set health checks to wait until postgres has started 36 | options: >- 37 | --health-cmd pg_isready 38 | --health-interval 10s 39 | --health-timeout 5s 40 | --health-retries 5 41 | 42 | redis: 43 | # Docker Hub image 44 | image: redis 45 | ports: 46 | - 6379:6379 47 | # Set health checks to wait until redis has started 48 | options: >- 49 | --health-cmd "redis-cli ping" 50 | --health-interval 10s 51 | --health-timeout 5s 52 | --health-retries 5 53 | 54 | steps: 55 | - uses: actions/checkout@v2 56 | - name: Set Up Submodule 57 | run: | 58 | git submodule init 59 | git submodule update 60 | 61 | - name: Set up Go 62 | uses: actions/setup-go@v2 63 | with: 64 | go-version: 1.15 65 | 66 | - name: Migrate 67 | run: go run main.go migrate_up 68 | env: 69 | DB_URI: postgres://twt:twt@localhost:5432/twt?sslmode=disable 70 | SECRET_KEY: mysecret 71 | MAX_FILE_SIZE: 30 72 | 73 | - name: test 74 | run: go test -v -p 1 -coverprofile=coverage.txt -covermode=atomic ./... 75 | env: 76 | DB_URI: postgres://twt:twt@localhost:5432/twt?sslmode=disable 77 | SECRET_KEY: mysecret 78 | MAX_FILE_SIZE: 30 79 | REDIS_HOST: localhost:6379 80 | REDIS_DB: 0 81 | - name: Upload to CodeCov 82 | run: bash <(curl -s https://codecov.io/bash) 83 | env: 84 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 85 | -------------------------------------------------------------------------------- /.github/workflows/status-webhook.yml: -------------------------------------------------------------------------------- 1 | name: Status Webhook 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Lint & Test 7 | - Build & Publish 8 | types: 9 | - completed 10 | 11 | 12 | jobs: 13 | send-embed: 14 | runs-on: ubuntu-latest 15 | name: Send an embed to discord 16 | 17 | steps: 18 | - name: Run the Github Actions Status Embed Action 19 | uses: SebastiaanZ/github-status-embed-for-discord@main 20 | with: 21 | webhook_id: '796006792995143710' 22 | webhook_token: ${{ secrets.WEBHOOK_TOKEN }} 23 | status: ${{ github.event.workflow_run.conclusion }} 24 | 25 | ref: ${{ github.ref }} 26 | actor: ${{ github.actor }} 27 | repository: ${{ github.repository }} 28 | run_id: ${{ github.event.workflow_run.id }} 29 | sha: ${{ github.event.workflow_run.head_sha }} 30 | workflow_name: ${{ github.event.workflow_run.name }} 31 | run_number: ${{ github.event.workflow_run.run_number }} 32 | 33 | pr_title: ${{ github.event.pull_request.title }} 34 | pr_number: ${{ github.event.pull_request.number }} 35 | pr_source: ${{ github.event.pull_request.head.label }} 36 | pr_author_login: ${{ github.event.pull_request.user.login }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | *.env 17 | # app.env 18 | .vscode/ 19 | .idea/ 20 | *.iml 21 | heap.tar.gz 22 | profile.tar.gz 23 | 24 | # Just for manual testing 25 | # *.rb 26 | *.py 27 | 28 | # docs 29 | docs.json 30 | 31 | docs/docs-template/node_modules/ 32 | docs/docs-template/public/build/ 33 | 34 | *.DS_Store 35 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "models"] 2 | path = models 3 | url = https://github.com/Tech-With-Tim/models.git 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine as docs 2 | 3 | WORKDIR /docs 4 | 5 | COPY ./docs/docs-template/package.json /docs 6 | COPY ./docs/docs-template/package-lock.json /docs 7 | 8 | RUN npm install 9 | 10 | COPY ./docs/docs-template /docs 11 | 12 | RUN npm run build 13 | 14 | # --------- 15 | 16 | FROM golang:1.16-alpine3.13 as builder 17 | WORKDIR /app 18 | 19 | ADD go.mod . 20 | ADD go.sum . 21 | RUN go mod download -x 22 | 23 | COPY . . 24 | COPY --from=docs /docs/public/ /app/docs/docs-template/public/ 25 | 26 | RUN go run main.go generate_docs 27 | RUN CGO_ENABLED=0 GOOS=linux go build -o main -ldflags "-w -s" 28 | 29 | # -------- 30 | 31 | FROM alpine 32 | WORKDIR /app 33 | COPY --from=builder /app/ /app/ 34 | RUN chmod 755 ./main 35 | 36 | EXPOSE 5000 37 | CMD ["/app/main", "runserver", "--host", "0.0.0.0"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tech With Tim Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include app.env 2 | # postgres: 3 | # docker run --name postgres12 -p 5432:5432 -e POSTGRES_USER=${POSTGRES_USER} -e POSTGRES_PASSWORD=${POSTGRES_PASSWORD} -d postgres:12-alpine 4 | 5 | redis: 6 | docker run --name redis3 -p 6379:6379 -d redis:6.2 redis-server 7 | 8 | # createdb: 9 | # docker exec -it postgres12 createdb --username=${POSTGRES_USER} --owner=${POSTGRES_USER} ${DB_NAME} 10 | 11 | # dropdb: 12 | # docker exec -it postgres12 dropdb -U ${POSTGRES_USER} ${DB_NAME} 13 | 14 | migrate_up: 15 | go run main.go migrate_up 16 | 17 | ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 18 | 19 | sqlc_generate: 20 | docker run --rm -v $(ROOT_DIR):/src -w /src kjconroy/sqlc generate 21 | 22 | generate_docs: 23 | @echo "Generating docs to docs.json" 24 | @go run main.go generate_docs 25 | @cd ./docs/docs-template; [ -d node_modules ] && echo "Skipping install of packages" || npm install 26 | @echo "Bulding docs template" 27 | @cd ./docs/docs-template && npm run build 28 | 29 | test: 30 | @go run main.go migrate_up -t | true 31 | @sh ./test.sh 32 | @echo "================================================" | GREP_COLOR='01;33' grep -E --color '^.*=.*' 33 | @printf "\033[33mCoverage\033[0m" 34 | @echo "" 35 | @sh ./cover.sh 36 | @echo "Cleaning..." 37 | @go run main.go dropdb -t 38 | # @echo "docker run --rm -v ${d}:/src -w /src kjconroy/sqlc generate" 39 | # createtestdb: 40 | # docker exec -it postgres12 createdb --username=sponge --owner=sponge twtTest 41 | 42 | # droptestdb: 43 | # docker exec -it postgres12 dropdb -U sponge twtTest 44 | 45 | .PHONY: migrate_up sqlc_generate test redis # createtestdb droptestdb 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project logo 2 | 3 |

Tech With Tim - CDN

4 | 5 |
6 | 7 | ![Status](https://img.shields.io/uptimerobot/status/m788529933-eaad92775b9eeb9753c9aac4) 8 | [![codecov](https://codecov.io/gh/Tech-With-Tim/cdn/branch/main/graph/badge.svg?token=YKpXOrUO80)](https://codecov.io/gh/Tech-With-Tim/cdn) 9 | [![Lint & Test](https://github.com/Tech-With-Tim/cdn/actions/workflows/lint-test.yml/badge.svg)](https://github.com/Tech-With-Tim/cdn/actions/workflows/lint-test.yml) 10 | [![GitHub Issues](https://img.shields.io/github/issues/Tech-With-Tim/CDN.svg)](https://github.com/Tech-With-Tim/CDN/issues) 11 | [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/Tech-With-Tim/CDN.svg)](https://github.com/Tech-With-Tim/CDN/pulls) 12 | [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](/LICENCE) 13 | [![Discord](https://discord.com/api/guilds/501090983539245061/widget.png?style=shield)](https://discord.gg/twt) 14 | 15 |
16 | 17 | CDN for the Tech With Tim website using [Go](https://go.dev/) 18 | 19 | ## 📝 Table of Contents 20 | - [🏁 Getting Started](#-getting-started) 21 | - [Environment variables](#environment-variables) 22 | - [Running](#running) 23 | - [🐳 Running with Docker](#-running-with-docker) 24 | - [🚨 Tests](#-tests) 25 | - [📜 Licence](/LICENCE) 26 | - [⛏️ Built Using](#️-built-using) 27 | - [✍️ Authors](#️-authors) 28 | 29 | 30 | ## 🏁 Getting Started 31 | 32 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [Running with Docker](#-running-with-docker) if you want to setup the CDN faster with Docker. ( Docker is optional ) 33 | 34 | ### Environment variables 35 | 36 | Set the environment variables. Start by writing this in a file named `app.env` and `test.env`: 37 | (test.env is required for running tests) 38 | 39 | app.env and test.env should look like this: 40 | 41 | ```prolog 42 | DB_URI=postgres://user:password@localhost:5432/dbname?sslmode=disable 43 | SECRET_KEY=secret 44 | MAX_FILE_SIZE=30 45 | ``` 46 | - ``SECRET_KEY`` is the key used for the JWT token encoding. 47 | - ``MAX_FILE_SIZE`` is the maxiumum file size allowed in asset upload (in mb) 48 | 49 | ### Running 50 | 51 | - To create the Postgres container - `make postgres` 52 | - To create the db - `make createdb` 53 | - To drop db - `make dropdb` 54 | 55 | #### Run `go mod tidy` to install packages 56 | #### CLI commands 57 | ``` 58 | go run main.go migrate_up 59 | go run main.go dropdb 60 | go run main.go migrate_steps --steps int 61 | go run main.go generate_docs 62 | go run main.go runserver --host localhost --port port (localhost, 5000 are default) 63 | ``` 64 | 65 | #### To run migrations on the test database 66 | ``` 67 | go run main.go migrate_up -t 68 | go run main.go dropdb -t 69 | go run main.go migrate_steps -t --steps int 70 | ``` 71 | 72 | ### Use the make file, its your best friend 🛠 73 | #### Make commands - 74 | If you are on windows please use Git Bash or WSL. You also have to install Make for Windows 75 | To install Make for Windows run `winget install GnuWin32.Make` 76 | 77 | ```shell 78 | make postgres # Creates docker container for postgres12 79 | # Reads env variables from app.env 80 | make createdb # Creates the db in the postgres container 81 | make dropdb # Drops the db 82 | make migrate_up # Migrates to the latest schema 83 | make sqlc_generate # Generates sqlc code if you write queries 84 | make generate_docs # Generates documentation 85 | make test # Tests your code and shows coverage 86 | # Its a big output make sure to read it all 87 | ``` 88 | 89 | ## 🐳 Running with Docker 90 | 91 | Start the cdn with `docker-compose up` 92 | 93 | ## 🗒️Docs 94 | 95 | While adding new endpoints, you need add docs in the form of comments. For example: 96 | ```go 97 | /* 98 | Response: String 99 | 100 | URL Parameters: None 101 | 102 | Request Body: 103 | - Name: username 104 | - Type: String 105 | - Description: "Username to register so and so . . ." 106 | 107 | Description: "Returns `Hello, World` when called." 108 | */ 109 | func GetAllAssets(w http.ResponseWriter, r *http.Request) { 110 | w.WriteHeader(statusCode) 111 | json.NewEncoder(w).Encode("Hello, World") 112 | } 113 | ``` 114 | 115 | And you will need to update the routes variable in [routes.go](/api/routes.go) 116 | 117 | ## 🚨 Tests 118 | There are two methods to test the cdn - 119 | ```sh 120 | make test 121 | ``` 122 | If you don't have make installed - 123 | ```sh 124 | go run main.go migrate_up -t 125 | go test ./... -v 126 | ``` 127 | **When you contribute, you need to add tests for the features you add.** 128 | 129 | ## ⛏️ Built Using 130 | 131 | - [Go](https://go.dev/) - Language 132 | - [go-chi](https://github.com/go-chi/chi) - Router 133 | - [sqlc](https://github.com/kyleconroy/sqlc) - Database Query Helper 134 | 135 | ## ✍️ Authors 136 | See the list of [contributors](https://github.com/Tech-With-Tim/cdn/contributors) who participated in this project. 137 | -------------------------------------------------------------------------------- /api/handlers/asset_details.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/Tech-With-Tim/cdn/utils" 10 | "github.com/go-chi/chi/v5" 11 | ) 12 | 13 | /* 14 | Response: JSON 15 | 16 | URL Parameters: path (String) 17 | 18 | Description: "Returns details of assets, given the path to the asset. If 19 | the asset is not found, a 404 error is raised" 20 | */ 21 | func (s *Service) FetchAssetDetailsByURL() http.HandlerFunc { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | var resp map[string]interface{} 24 | url := chi.URLParam(r, "path") 25 | fileRow, err := s.Store.GetAssetDetailsByUrl(r.Context(), url) 26 | 27 | // type GetAssetDetailsByUrlRow struct { 28 | // ID int64 `json:"id"` 29 | // Name string `json:"name"` 30 | // CreatorID int64 `json:"creatorID"` 31 | // } 32 | 33 | if err != nil { 34 | if err == sql.ErrNoRows { 35 | resp = map[string]interface{}{"error": "not found", 36 | "message": "no asset found with that url path."} 37 | utils.JSON(w, http.StatusNotFound, resp) 38 | return 39 | } 40 | resp = map[string]interface{}{"error": "something Unexpected Occurred."} 41 | utils.JSON(w, http.StatusInternalServerError, resp) 42 | log.Println(err.Error()) 43 | return 44 | } 45 | utils.JSON(w, http.StatusOK, fileRow) 46 | } 47 | } 48 | 49 | /* 50 | Response: JSON 51 | 52 | URL Parameters: id (Integer) 53 | 54 | Description: "Returns details of assets, given the asset ID. 55 | If it finds the asset, it returns JSON containing info about 56 | the asset. If the asset is not found, a 404 error is raised. 57 | If the ID provided is not an integer, a 400 error is raised." 58 | */ 59 | func (s *Service) FetchAssetDetailsByID() http.HandlerFunc { 60 | return func(w http.ResponseWriter, r *http.Request) { 61 | var resp map[string]interface{} 62 | id, err := strconv.Atoi(chi.URLParam(r, "id")) 63 | if err != nil { 64 | resp = map[string]interface{}{"error": "id is not a valid integer."} 65 | utils.JSON(w, http.StatusBadRequest, resp) 66 | log.Println(err.Error()) 67 | return 68 | } 69 | 70 | fileRow, err := s.Store.GetAssetDetailsById(r.Context(), int64(id)) 71 | // type GetAssetDetailsByIdRow struct { 72 | // UrlPath string `json:"urlPath"` 73 | // Name string `json:"name"` 74 | // CreatorID int64 `json:"creatorID"` 75 | // } 76 | if err != nil { 77 | if err == sql.ErrNoRows { 78 | resp = map[string]interface{}{"error": "not found", 79 | "message": "no asset found with that id."} 80 | utils.JSON(w, http.StatusNotFound, resp) 81 | return 82 | } 83 | resp = map[string]interface{}{"error": "something Unexpected Occurred."} 84 | utils.JSON(w, http.StatusInternalServerError, resp) 85 | log.Println(err.Error()) 86 | return 87 | } 88 | utils.JSON(w, http.StatusOK, fileRow) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /api/handlers/create_asset.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "io/ioutil" 7 | "log" 8 | "mime" 9 | "mime/multipart" 10 | "net/http" 11 | "strconv" 12 | 13 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 14 | "github.com/Tech-With-Tim/cdn/utils" 15 | "github.com/omeid/pgerror" 16 | "golang.org/x/sync/errgroup" 17 | ) 18 | 19 | /* 20 | getUrlPath generates a random url path if url path is not provided 21 | in the request. 22 | */ 23 | func getUrlPath(url string, fileExt []string) string { 24 | if url == "" { 25 | if len(fileExt) != 0 { 26 | if fileExt[0] != "" { 27 | return utils.RandomString(16) + fileExt[0] 28 | } 29 | return utils.RandomString(16) 30 | } 31 | return utils.RandomString(16) 32 | } 33 | return url 34 | 35 | } 36 | 37 | // storeAsset Creates and stores asset in the database 38 | func storeAsset(mimetype string, 39 | fileName string, 40 | fileData []byte, 41 | assetName string, 42 | assetUrlPath string, 43 | userId int, 44 | store *db.Store, 45 | w http.ResponseWriter, 46 | r *http.Request) (assetId int64, err error) { 47 | var resp map[string]interface{} 48 | params := db.CreateAssetParams{ 49 | Mimetype: mimetype, 50 | Name: fileName, 51 | Data: fileData, 52 | Name_2: assetName, //Asset Name 53 | UrlPath: assetUrlPath, 54 | CreatorID: int64(userId), 55 | } 56 | assetId, err = store.CreateAssetFile(r.Context(), params) 57 | if err != nil { 58 | if e := pgerror.UniqueViolation(err); e != nil { 59 | resp = map[string]interface{}{"error": "Conflict", 60 | "message": "Asset with this url_path already exists."} 61 | utils.JSON(w, http.StatusConflict, resp) 62 | return 63 | } 64 | resp = map[string]interface{}{"error": "Something unexpected occurred."} 65 | utils.JSON(w, http.StatusInternalServerError, resp) 66 | log.Println(err.Error()) 67 | return 68 | } 69 | return 70 | } 71 | 72 | /* 73 | Response: JSON 74 | 75 | URL Parameters: None 76 | 77 | Request Body: 78 | - Name: name 79 | Type: String 80 | Description: "Name under which to store the asset in the CDN." 81 | - Name: url_path 82 | Type: String 83 | Description: "The URL Path under which to store the asset in the CDN. 84 | If none is provided, a random path is selected." 85 | 86 | Description: "Create Asset creates an asset with a given file, 87 | uploaded under the `data` parameter as a form file. 88 | If it succeeds, it returns a 201 Created Status. If the FileSize 89 | is too large, a 413 error is raised. If the file is not provided, 90 | a 400 error is raised." 91 | */ 92 | func (s *Service) CreateAsset(FileSize int64) http.HandlerFunc { 93 | return func(w http.ResponseWriter, r *http.Request) { 94 | var resp map[string]interface{} 95 | fileData := make(chan []byte) 96 | var assetId int64 97 | var urlPath string 98 | g, ctx := errgroup.WithContext(r.Context()) 99 | 100 | r.Body = http.MaxBytesReader(w, r.Body, FileSize<<20) 101 | //Parse Form 102 | err := r.ParseMultipartForm(FileSize << 20) 103 | if err != nil { 104 | resp = map[string]interface{}{"error": err.Error()} 105 | utils.JSON(w, http.StatusRequestEntityTooLarge, resp) 106 | return 107 | } 108 | 109 | upload, handler, err := r.FormFile("data") 110 | if err != nil { 111 | resp = map[string]interface{}{"error": "No file in 'data' field"} 112 | utils.JSON(w, http.StatusBadRequest, resp) 113 | return 114 | } 115 | defer func(upload multipart.File) { 116 | err := upload.Close() 117 | if err != nil { 118 | log.Println(err.Error()) 119 | } 120 | }(upload) 121 | 122 | g.Go(func() error { 123 | defer close(fileData) 124 | fileBytes, er := ioutil.ReadAll(upload) 125 | if er != nil { 126 | log.Println(er.Error()) 127 | resp = map[string]interface{}{"error": "Something unexpected occurred."} 128 | utils.JSON(w, http.StatusInternalServerError, resp) 129 | return er 130 | } 131 | fileData <- fileBytes 132 | return nil 133 | 134 | }) 135 | //var params db.CreateAssetParams 136 | g.Go(func() error { 137 | var fileExt []string 138 | 139 | fileName := handler.Filename 140 | 141 | assetName := r.FormValue("name") 142 | mimetype := handler.Header.Get("Content-Type") 143 | //File Extension 144 | fileExt, _ = mime.ExtensionsByType(mimetype) 145 | userId := ctx.Value("uid").(int) 146 | urlPath = getUrlPath(html.EscapeString(r.FormValue("url_path")), fileExt) 147 | //Storing asset in database 148 | assetId, err = storeAsset(mimetype, fileName, <-fileData, 149 | assetName, urlPath, userId, s.Store, w, r) 150 | if err != nil { 151 | return err 152 | } 153 | return nil 154 | }) 155 | 156 | if err = g.Wait(); err != nil { 157 | return 158 | } 159 | resp = map[string]interface{}{"location": fmt.Sprintf("%s/%s", r.Host, urlPath), 160 | "asset_id": strconv.Itoa(int(assetId))} 161 | 162 | utils.JSON(w, http.StatusCreated, resp) 163 | 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /api/handlers/crud_methods.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | 6 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 7 | ) 8 | 9 | func (s *stores) getFile(url string, 10 | ctx context.Context) (fileRow db.GetFileRow, err error) { 11 | 12 | cachedFile := s.Cache.Get(url) 13 | if cachedFile == nil { 14 | fileRow, err = s.Store.GetFile(ctx, url) 15 | 16 | if err != nil { 17 | return 18 | } 19 | s.Cache.Set(url, &fileRow) 20 | 21 | } else { 22 | fileRow = *cachedFile 23 | } 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /api/handlers/get_asset.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "net/http" 7 | 8 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 9 | "github.com/Tech-With-Tim/cdn/utils" 10 | "github.com/go-chi/chi/v5" 11 | ) 12 | 13 | /* 14 | Response: FileType | JSON 15 | 16 | URL Parameters: AssetURL (string) 17 | 18 | Description: "Return an asset, given the asset url. If the asset 19 | is not found, a 404 error is returned. If it is found, the 20 | asset is returned as per it's file type." 21 | */ 22 | func (s *Service) GetAsset() http.HandlerFunc { 23 | return func(w http.ResponseWriter, r *http.Request) { 24 | var resp map[string]interface{} 25 | var fileRow db.GetFileRow 26 | var err error 27 | 28 | url := chi.URLParam(r, "AssetUrl") 29 | 30 | fileRow, err = s.getFile(url, r.Context()) 31 | 32 | if err != nil { 33 | if err == sql.ErrNoRows { 34 | 35 | resp = map[string]interface{}{"error": "Not Found", 36 | "message": "No asset found with that url_path."} 37 | 38 | utils.JSON(w, http.StatusNotFound, resp) 39 | return 40 | } 41 | 42 | resp = map[string]interface{}{"error": "Something Unexpected Occurred."} 43 | 44 | utils.JSON(w, http.StatusInternalServerError, resp) 45 | 46 | log.Println(err.Error()) 47 | 48 | return 49 | } 50 | 51 | // FileRow: 52 | // Data []byte `json:"data"` 53 | // Mimetype string `json:"mimetype"` 54 | 55 | w.Header().Set("Content-Type", fileRow.Mimetype) 56 | _, err = w.Write(fileRow.Data) 57 | if err != nil { 58 | resp = map[string]interface{}{"error": "Something Unexpected Occurred."} 59 | utils.JSON(w, http.StatusInternalServerError, resp) 60 | log.Println(err.Error()) 61 | return 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /api/handlers/handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/Tech-With-Tim/cdn/cache" 8 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 9 | ) 10 | 11 | type stores struct { 12 | Store *db.Store 13 | Cache cache.PostCache 14 | } 15 | 16 | type Service struct { 17 | *stores 18 | } 19 | 20 | type DBHandler interface { 21 | getFile(url string, ctx context.Context) (db.GetFileRow, error) 22 | } 23 | 24 | type Handler interface { 25 | FetchAssetDetailsByURL() http.HandlerFunc 26 | FetchAssetDetailsByID() http.HandlerFunc 27 | CreateAsset(FileSize int64) http.HandlerFunc 28 | GetAsset() http.HandlerFunc 29 | } 30 | 31 | func NewServiceHandler(store *db.Store, cache cache.PostCache) Handler { 32 | return &Service{ 33 | &stores{ 34 | Store: store, 35 | Cache: cache, 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /api/handlers/hello_world.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | /* 9 | Response: String 10 | 11 | URL Parameters: None 12 | 13 | Description: "Returns `Hello, World!` when called. This route is for 14 | testing purposes only." 15 | */ 16 | func HelloWorld() http.HandlerFunc { 17 | return func(w http.ResponseWriter, r *http.Request) { 18 | //fmt.Println(r.Header) 19 | _, err := w.Write([]byte("Hello, World!")) 20 | if err != nil { 21 | log.Println(err.Error()) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/handlers_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "net/http/httptest" 13 | "strconv" 14 | "testing" 15 | "time" 16 | 17 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 18 | 19 | "github.com/Tech-With-Tim/cdn/utils" 20 | "github.com/golang-jwt/jwt" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | type assetDetailsJsonResponse struct { 25 | Location string `json:"location"` 26 | AssetId string `json:"asset_id"` 27 | } 28 | 29 | func executeRequest(req *http.Request) *httptest.ResponseRecorder { 30 | rr := httptest.NewRecorder() 31 | s.Router.ServeHTTP(rr, req) 32 | 33 | return rr 34 | } 35 | 36 | func checkResponseCode(t *testing.T, expected, actual int) { 37 | if expected != actual { 38 | t.Errorf("Expected response code %d. Got %d\n", expected, actual) 39 | } 40 | } 41 | 42 | func createAuthToken(exp int64) (string, error) { 43 | claims := jwt.MapClaims{} 44 | 45 | claims["uid"] = fmt.Sprintf("%v", 46 | 328604827967815690) 47 | claims["exp"] = exp //time.Now().Add(time.Hour * 24).Unix() 48 | claims["IssuedAt"] = time.Now().Unix() 49 | token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims) 50 | return token.SignedString([]byte(config.SecretKey)) 51 | } 52 | 53 | func createRandomAsset(t *testing.T, authToken string) (string, *httptest.ResponseRecorder, []byte) { 54 | payload := &bytes.Buffer{} 55 | writer := multipart.NewWriter(payload) 56 | bytesData := utils.StrToBinary(utils.RandomString(100), 10) 57 | bytesReader := bytes.NewReader(bytesData) 58 | formFile, err := writer.CreateFormFile("data", utils.RandomString(4)) 59 | require.NoError(t, err) 60 | _, err = io.Copy(formFile, bytesReader) 61 | require.NoError(t, err) 62 | assetName := utils.RandomString(5) 63 | _ = writer.WriteField("name", assetName) 64 | err = writer.Close() 65 | require.NoError(t, err) 66 | req, _ := http.NewRequest("POST", "/manage", payload) 67 | req.Header.Add("Authorization", authToken) 68 | 69 | req.Header.Set("Content-Type", writer.FormDataContentType()) 70 | response := executeRequest(req) 71 | return assetName, response, bytesData 72 | } 73 | 74 | func TestHelloWorld(t *testing.T) { 75 | req, _ := http.NewRequest("GET", "/testing", nil) 76 | 77 | token, err := createAuthToken(time.Now().Add(time.Hour * 24).Unix()) 78 | require.NoError(t, err) 79 | response := executeRequest(req) 80 | checkResponseCode(t, http.StatusUnauthorized, response.Code) 81 | req.Header.Add("Authorization", token) 82 | response = executeRequest(req) 83 | checkResponseCode(t, http.StatusOK, response.Code) 84 | if body := response.Body.String(); body != "Hello, World!" { 85 | t.Errorf("Expected Hello World. Got %s", body) 86 | } 87 | token, err = createAuthToken(time.Now().Unix() - 60) 88 | 89 | require.NoError(t, err) 90 | req.Header.Set("Authorization", token) 91 | response = executeRequest(req) 92 | checkResponseCode(t, http.StatusUnauthorized, response.Code) 93 | } 94 | 95 | func TestCreateAsset(t *testing.T) { 96 | authToken, err := createAuthToken(time.Now().Add(time.Hour * 24).Unix()) 97 | require.NoError(t, err) 98 | assetNameChan := make(chan string) 99 | responseChan := make(chan *httptest.ResponseRecorder) 100 | fileDataChan := make(chan []byte) 101 | n := 5 102 | var res *httptest.ResponseRecorder 103 | var assetName string 104 | 105 | var fileReq *http.Request 106 | var fileRes *httptest.ResponseRecorder 107 | var receivedFileData []byte 108 | var originalFileData []byte 109 | var body []byte 110 | var assetId int 111 | var AssetDetails db.GetAssetDetailsByIdRow 112 | 113 | for i := 0; i < n; i++ { 114 | go func() { 115 | assetN, response, fileData := createRandomAsset(t, authToken) 116 | assetNameChan <- assetN 117 | responseChan <- response 118 | fileDataChan <- fileData 119 | }() 120 | } 121 | for l := 0; l < n; l++ { 122 | assetName = <-assetNameChan 123 | res = <-responseChan 124 | originalFileData = <-fileDataChan 125 | 126 | checkResponseCode(t, http.StatusCreated, res.Code) 127 | require.NotEmpty(t, res.Body.String()) 128 | body, err = ioutil.ReadAll(res.Body) 129 | require.NoError(t, err) 130 | 131 | //Check store assets details 132 | assetResponse := &assetDetailsJsonResponse{} 133 | err = json.Unmarshal(body, &assetResponse) 134 | require.NoError(t, err) 135 | assetId, err = strconv.Atoi(assetResponse.AssetId) 136 | require.NoError(t, err) 137 | AssetDetails, err = s.Store.GetAssetDetailsById(context.Background(), int64(assetId)) 138 | require.NoError(t, err) 139 | require.NotEmpty(t, AssetDetails) 140 | require.Equal(t, AssetDetails.Name, assetName) //AssetDetails.Name 141 | 142 | //Check stored files byte data 143 | fileReq, err = http.NewRequest("GET", assetResponse.Location, nil) 144 | require.NoError(t, err) 145 | fileRes = executeRequest(fileReq) 146 | checkResponseCode(t, http.StatusOK, fileRes.Code) 147 | receivedFileData, err = ioutil.ReadAll(fileRes.Body) 148 | require.NoError(t, err) 149 | require.Equal(t, originalFileData, receivedFileData) 150 | 151 | // endpoint /manage/url 152 | // check if the info is correct 153 | fileReq, err = http.NewRequest("GET", "/manage/url/"+AssetDetails.UrlPath, nil) 154 | require.NoError(t, err) 155 | fileRes = executeRequest(fileReq) 156 | checkResponseCode(t, http.StatusOK, fileRes.Code) 157 | receivedFileData, err = ioutil.ReadAll(fileRes.Body) 158 | require.NoError(t, err) 159 | 160 | manageURLResponse := &db.GetAssetDetailsByUrlRow{} 161 | err = json.Unmarshal(receivedFileData, manageURLResponse) 162 | require.NoError(t, err) 163 | require.Equal(t, assetId, int(manageURLResponse.ID)) 164 | require.Equal(t, assetName, manageURLResponse.Name) 165 | require.Equal(t, AssetDetails.CreatorID, manageURLResponse.CreatorID) 166 | 167 | // not found 168 | // um, hopefully there isn't a url named * in the test db. 169 | fileReq, err = http.NewRequest("GET", "/manage/url/*", nil) 170 | require.NoError(t, err) 171 | fileRes = executeRequest(fileReq) 172 | checkResponseCode(t, http.StatusNotFound, fileRes.Code) 173 | 174 | // endpoint /manage/id 175 | // check if the info is correct 176 | fileReq, err = http.NewRequest("GET", "/manage/id/"+strconv.Itoa(assetId), nil) 177 | require.NoError(t, err) 178 | fileRes = executeRequest(fileReq) 179 | checkResponseCode(t, http.StatusOK, fileRes.Code) 180 | receivedFileData, err = ioutil.ReadAll(fileRes.Body) 181 | require.NoError(t, err) 182 | 183 | manageIDResponse := &db.GetAssetDetailsByIdRow{} 184 | err = json.Unmarshal(receivedFileData, manageIDResponse) 185 | require.NoError(t, err) 186 | require.Equal(t, AssetDetails.UrlPath, manageIDResponse.UrlPath) 187 | require.Equal(t, assetName, manageIDResponse.Name) 188 | require.Equal(t, AssetDetails.CreatorID, manageIDResponse.CreatorID) 189 | 190 | // not found 191 | // um, hopefully there isn't a id named 1 in the test db. 192 | fileReq, err = http.NewRequest("GET", "/manage/id/1", nil) 193 | require.NoError(t, err) 194 | fileRes = executeRequest(fileReq) 195 | checkResponseCode(t, http.StatusNotFound, fileRes.Code) 196 | 197 | // not int 198 | fileReq, err = http.NewRequest("GET", "/manage/id/abc", nil) 199 | require.NoError(t, err) 200 | fileRes = executeRequest(fileReq) 201 | checkResponseCode(t, http.StatusBadRequest, fileRes.Code) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /api/main_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/Tech-With-Tim/cdn/api/handlers" 10 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 11 | "github.com/Tech-With-Tim/cdn/server" 12 | "github.com/Tech-With-Tim/cdn/utils" 13 | "github.com/go-chi/chi/v5" 14 | ) 15 | 16 | var config *utils.Config 17 | var s *server.Server 18 | 19 | func TestMain(m *testing.M) { 20 | conf, err := utils.LoadConfig("../", "test") 21 | if err != nil { 22 | log.Fatalf("error: %v", err.Error()) 23 | } 24 | config = &conf 25 | s = server.NewServer(conf) 26 | CdnRouter := chi.NewRouter() 27 | //Add Routes to Routers Here 28 | services := handlers.NewServiceHandler(s.Store, *s.Cache) 29 | 30 | MainRouter(CdnRouter, conf, services) 31 | 32 | //Mount Routers here 33 | s.Router.Mount("/", CdnRouter) 34 | err = createTestUser() 35 | if err != nil { 36 | log.Fatalf("error: %v", err.Error()) 37 | } 38 | 39 | os.Exit(m.Run()) 40 | 41 | } 42 | 43 | func createTestUser() error { 44 | user := db.CreateUserParams{ 45 | ID: 328604827967815690, 46 | Username: utils.RandomString(4), 47 | Discriminator: "3212", 48 | } 49 | err := s.Store.CreateUser(context.Background(), user) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /api/middleware.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/Tech-With-Tim/cdn/utils" 11 | "github.com/golang-jwt/jwt" 12 | ) 13 | 14 | const errorstring string = "The server could not verify that you are authorized to access the URL requested. " + 15 | "You either supplied the wrong credentials (e.g. a bad password), " + 16 | "or your browser doesn't understand how to supply the credentials required." 17 | 18 | func AuthJwtWrap(SecretKey string) func(next http.Handler) http.Handler { 19 | return func(next http.Handler) http.Handler { 20 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | var resp = map[string]interface{}{"error": "unauthorized", "message": "missing authorization token"} 22 | var header = r.Header.Get("Authorization") 23 | header = strings.TrimSpace(header) 24 | if header == "" { 25 | utils.JSON(w, http.StatusUnauthorized, resp) 26 | return 27 | } 28 | 29 | //utils.ExportVariables() 30 | token, err := jwt.Parse(header, func(token *jwt.Token) (interface{}, error) { 31 | return []byte(SecretKey), nil 32 | }) 33 | 34 | if err != nil { 35 | resp["error"] = "unauthorized" 36 | if err.Error() == "Token is expired" { 37 | resp["message"] = err.Error() 38 | utils.JSON(w, http.StatusUnauthorized, resp) 39 | return 40 | } 41 | resp["message"] = errorstring 42 | utils.JSON(w, http.StatusUnauthorized, resp) 43 | log.Println(err.Error()) 44 | return 45 | } 46 | 47 | claims, _ := token.Claims.(jwt.MapClaims) 48 | 49 | uid, err := strconv.Atoi(claims["uid"].(string)) 50 | //fmt.Println(sub) 51 | if err != nil { 52 | resp["error"] = "something unexpected occurred" 53 | utils.JSON(w, http.StatusInternalServerError, resp) 54 | log.Println(err.Error()) 55 | return 56 | } 57 | 58 | ctx := context.WithValue(r.Context(), "uid", uid) // adding the user ID to the context 59 | next.ServeHTTP(w, r.WithContext(ctx)) 60 | 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /api/routes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/Tech-With-Tim/cdn/api/handlers" 7 | "github.com/Tech-With-Tim/cdn/utils" 8 | "github.com/go-chi/chi/v5" 9 | ) 10 | 11 | // var postCache cache.PostCache 12 | 13 | func MainRouter(r *chi.Mux, config utils.Config, s handlers.Handler) { 14 | 15 | // postCache = cache.NewRedisCache( 16 | // config.RedisHost, 17 | // config.RedisDb, 18 | // config.RedisPass, 19 | // 60) 20 | 21 | r.Group(func(r chi.Router) { 22 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 23 | http.Redirect(w, r, "/docs", http.StatusPermanentRedirect) 24 | }) 25 | r.Get("/{AssetUrl}", s.GetAsset()) 26 | r.Get("/manage/url/{path}", s.FetchAssetDetailsByURL()) 27 | r.Get("/manage/id/{id}", s.FetchAssetDetailsByID()) 28 | }) 29 | 30 | //Private Routes 31 | r.Group(func(r chi.Router) { 32 | r.Use(AuthJwtWrap(config.SecretKey)) 33 | r.Post("/manage", s.CreateAsset(config.MaxFileSize)) 34 | r.Get("/testing", handlers.HelloWorld()) 35 | }) 36 | } 37 | 38 | // Method Route - Handler Function Name 39 | var Routes map[string]string = map[string]string{ 40 | "GET /{AssetUrl}": "Get Asset", 41 | "GET /manage/url/{path}": "Fetch Asset Details By URL", 42 | "GET /manage/id/{id}": "Fetch Asset Details By ID", 43 | "POST /manage": "Create Asset", 44 | } 45 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 11 | "github.com/Tech-With-Tim/cdn/utils" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | var postCache PostCache 16 | var testQueries *db.Queries 17 | 18 | func createRandomAsset(t *testing.T) (db.CreateAssetParams, int64) { 19 | arg := db.CreateAssetParams{ 20 | Mimetype: "application/octet-stream", 21 | Name: utils.RandomString(4) + ".bin", //FileName 22 | Data: utils.StrToBinary(utils.RandomString(16), 10), 23 | Name_2: utils.RandomString(4), //AssetName 24 | UrlPath: utils.RandomString(4), 25 | CreatorID: 735376244656308212, 26 | } 27 | //Context.Background() is to provide empty Context For tests 28 | assetId, err := testQueries.CreateAsset(context.Background(), arg) 29 | require.NoError(t, err) 30 | require.NotEmpty(t, assetId) 31 | require.NotZero(t, assetId) 32 | return arg, assetId 33 | } 34 | func cleanup(t *testing.T, asset db.CreateAssetParams) { 35 | deleteArgs := db.DeleteAssetParams{ 36 | UrlPath: asset.UrlPath, 37 | CreatorID: asset.CreatorID, 38 | } 39 | err := testQueries.DeleteAsset(context.Background(), deleteArgs) 40 | require.NoError(t, err) 41 | } 42 | 43 | func createTestUser(t *testing.T) { 44 | user := db.CreateUserParams{ 45 | ID: 735376244656308212, 46 | Username: utils.RandomString(4), 47 | Discriminator: "9123", 48 | } 49 | err := testQueries.CreateUser(context.Background(), user) 50 | require.NoError(t, err) 51 | } 52 | 53 | func TestMain(m *testing.M) { 54 | config, err := utils.LoadConfig("../", "test") 55 | if err != nil { 56 | log.Fatalln(err.Error()) 57 | } 58 | if config.RedisHost == "" { 59 | os.Exit(0) 60 | } 61 | dbSource := config.DBUri 62 | testDB, err := sql.Open("postgres", dbSource) 63 | if err != nil { 64 | log.Fatalln(err.Error()) 65 | } 66 | testQueries = db.New(testDB) 67 | postCache = NewRedisCache(config.RedisHost, config.RedisDb, config.RedisPass, 1) 68 | os.Exit(m.Run()) 69 | } 70 | 71 | func TestRedisCache_Set(t *testing.T) { 72 | createTestUser(t) 73 | var n = 10 74 | fileRowChan := make(chan db.GetFileRow) 75 | errChan := make(chan error) 76 | randAssetChan := make(chan db.CreateAssetParams) 77 | var fileRow db.GetFileRow 78 | var err error 79 | var cachedRow *db.GetFileRow 80 | var randomAsset db.CreateAssetParams 81 | for i := 0; i < n; i++ { 82 | go func() { 83 | randomAsset, _ := createRandomAsset(t) 84 | fileRow, err := testQueries.GetFile(context.Background(), randomAsset.UrlPath) 85 | postCache.Set(randomAsset.UrlPath, &fileRow) 86 | randAssetChan <- randomAsset 87 | fileRowChan <- fileRow 88 | errChan <- err 89 | }() 90 | } 91 | 92 | for l := 0; l < n; l++ { 93 | randomAsset = <-randAssetChan 94 | fileRow = <-fileRowChan 95 | err = <-errChan 96 | require.NoError(t, err) 97 | require.NotEmpty(t, fileRow) 98 | require.Equal(t, randomAsset.Data, fileRow.Data) 99 | cachedRow = postCache.Get(randomAsset.UrlPath) 100 | require.Equal(t, fileRow, *cachedRow) 101 | cleanup(t, randomAsset) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /cache/posts-cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import db "github.com/Tech-With-Tim/cdn/db/sqlc" 4 | 5 | // PostCache Implements Cache Functions 6 | type PostCache interface { 7 | Set(key string, value *db.GetFileRow) 8 | Get(key string) *db.GetFileRow 9 | } 10 | -------------------------------------------------------------------------------- /cache/redis-cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | json2 "encoding/json" 5 | "log" 6 | "time" 7 | 8 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 9 | "github.com/go-redis/redis/v7" 10 | ) 11 | 12 | type redisCache struct { 13 | host string 14 | db int 15 | pass string 16 | expires time.Duration 17 | client *redis.Client 18 | } 19 | 20 | func NewRedisCache(host string, db int, pass string, expires time.Duration) PostCache { 21 | cache := &redisCache{ 22 | host: host, 23 | db: db, 24 | pass: pass, 25 | expires: expires, 26 | } 27 | cache.getClient() 28 | return cache 29 | } 30 | 31 | func (cache *redisCache) getClient() { 32 | log.Println("Trying to connect to redis") 33 | cache.client = redis.NewClient(&redis.Options{ 34 | Addr: cache.host, 35 | Password: cache.pass, 36 | DB: cache.db, 37 | }) 38 | } 39 | 40 | func (cache *redisCache) Set(key string, value *db.GetFileRow) { 41 | json, err := json2.Marshal(value) 42 | if err != nil { 43 | log.Println(err.Error()) 44 | } 45 | log.Printf("Added to cache: %s", key) 46 | cache.client.Set(key, json, cache.expires*time.Minute) 47 | } 48 | 49 | func (cache *redisCache) Get(key string) *db.GetFileRow { 50 | 51 | val, err := cache.client.Get(key).Result() 52 | if err != nil { 53 | return nil 54 | } 55 | fileRow := db.GetFileRow{} 56 | err = json2.Unmarshal([]byte(val), &fileRow) 57 | if err != nil { 58 | log.Println(err.Error()) 59 | } 60 | return &fileRow 61 | } 62 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: up 7 | range: "50...100" 8 | status: 9 | patch: 10 | default: 11 | target: 50% 12 | parsers: 13 | gcov: 14 | branch_detection: 15 | conditional: yes 16 | loop: yes 17 | method: no 18 | macro: no 19 | 20 | comment: 21 | layout: "reach,diff,flags,files,footer" 22 | behavior: default 23 | require_changes: no 24 | -------------------------------------------------------------------------------- /cover.sh: -------------------------------------------------------------------------------- 1 | go tool cover -func cover.out | GREP_COLOR='01;32' grep -E --color 'github.*$|^.*github.*$|^.*total.*$|$ ' -------------------------------------------------------------------------------- /db/query/cdn.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateAsset :one 2 | WITH fid AS ( 3 | INSERT into files (id, mimetype, name, data) 4 | VALUES (create_snowflake(), $1, $2, $3) 5 | RETURNING id 6 | ) 7 | INSERT INTO assets (id, name, url_path, file_id, creator_id) 8 | VALUES (create_snowflake(), $4, $5, (SELECT id from fid), $6) 9 | RETURNING id; 10 | 11 | -- name: GetFile :one 12 | SELECT data, mimetype 13 | FROM files f 14 | WHERE f.id = ( 15 | SELECT file_id 16 | FROM assets a 17 | WHERE a.url_path = $1 18 | ); 19 | 20 | -- name: GetAssetDetailsByUrl :one 21 | SELECT id, name, creator_id 22 | FROM assets 23 | WHERE url_path = $1; 24 | 25 | -- name: GetAssetDetailsById :one 26 | SELECT url_path, name, creator_id 27 | FROM assets 28 | WHERE id = $1; 29 | 30 | -- name: DeleteAsset :exec 31 | DELETE 32 | FROM files 33 | WHERE id = (SELECT file_id FROM assets WHERE url_path = $1 AND creator_id = $2); 34 | 35 | -- name: ListAssetByCreator :many 36 | SELECT * 37 | FROM assets 38 | WHERE creator_id = $1 39 | ORDER BY id 40 | LIMIT $2 -- PageSize 41 | OFFSET $3; -- ((Pagenumber - 1) * PageSize) 42 | 43 | 44 | -- name: CreateUser :exec 45 | INSERT INTO users (id, username, discriminator) VALUES ($1, $2, $3); -------------------------------------------------------------------------------- /db/sqlc/cdn.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // source: cdn.sql 3 | 4 | package db 5 | 6 | import ( 7 | "context" 8 | ) 9 | 10 | const createAsset = `-- name: CreateAsset :one 11 | WITH fid AS ( 12 | INSERT into files (id, mimetype, name, data) 13 | VALUES (create_snowflake(), $1, $2, $3) 14 | RETURNING id 15 | ) 16 | INSERT INTO assets (id, name, url_path, file_id, creator_id) 17 | VALUES (create_snowflake(), $4, $5, (SELECT id from fid), $6) 18 | RETURNING id 19 | ` 20 | 21 | type CreateAssetParams struct { 22 | Mimetype string `json:"mimetype"` 23 | Name string `json:"name"` 24 | Data []byte `json:"data"` 25 | Name_2 string `json:"name2"` 26 | UrlPath string `json:"urlPath"` 27 | CreatorID int64 `json:"creatorID"` 28 | } 29 | 30 | func (q *Queries) CreateAsset(ctx context.Context, arg CreateAssetParams) (int64, error) { 31 | row := q.db.QueryRowContext(ctx, createAsset, 32 | arg.Mimetype, 33 | arg.Name, 34 | arg.Data, 35 | arg.Name_2, 36 | arg.UrlPath, 37 | arg.CreatorID, 38 | ) 39 | var id int64 40 | err := row.Scan(&id) 41 | return id, err 42 | } 43 | 44 | const createUser = `-- name: CreateUser :exec 45 | 46 | 47 | INSERT INTO users (id, username, discriminator) VALUES ($1, $2, $3) 48 | ` 49 | 50 | type CreateUserParams struct { 51 | ID int64 `json:"id"` 52 | Username string `json:"username"` 53 | Discriminator string `json:"discriminator"` 54 | } 55 | 56 | // ((Pagenumber - 1) * PageSize) 57 | func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error { 58 | _, err := q.db.ExecContext(ctx, createUser, arg.ID, arg.Username, arg.Discriminator) 59 | return err 60 | } 61 | 62 | const deleteAsset = `-- name: DeleteAsset :exec 63 | DELETE 64 | FROM files 65 | WHERE id = (SELECT file_id FROM assets WHERE url_path = $1 AND creator_id = $2) 66 | ` 67 | 68 | type DeleteAssetParams struct { 69 | UrlPath string `json:"urlPath"` 70 | CreatorID int64 `json:"creatorID"` 71 | } 72 | 73 | func (q *Queries) DeleteAsset(ctx context.Context, arg DeleteAssetParams) error { 74 | _, err := q.db.ExecContext(ctx, deleteAsset, arg.UrlPath, arg.CreatorID) 75 | return err 76 | } 77 | 78 | const getAssetDetailsById = `-- name: GetAssetDetailsById :one 79 | SELECT url_path, name, creator_id 80 | FROM assets 81 | WHERE id = $1 82 | ` 83 | 84 | type GetAssetDetailsByIdRow struct { 85 | UrlPath string `json:"urlPath"` 86 | Name string `json:"name"` 87 | CreatorID int64 `json:"creatorID"` 88 | } 89 | 90 | func (q *Queries) GetAssetDetailsById(ctx context.Context, id int64) (GetAssetDetailsByIdRow, error) { 91 | row := q.db.QueryRowContext(ctx, getAssetDetailsById, id) 92 | var i GetAssetDetailsByIdRow 93 | err := row.Scan(&i.UrlPath, &i.Name, &i.CreatorID) 94 | return i, err 95 | } 96 | 97 | const getAssetDetailsByUrl = `-- name: GetAssetDetailsByUrl :one 98 | SELECT id, name, creator_id 99 | FROM assets 100 | WHERE url_path = $1 101 | ` 102 | 103 | type GetAssetDetailsByUrlRow struct { 104 | ID int64 `json:"id"` 105 | Name string `json:"name"` 106 | CreatorID int64 `json:"creatorID"` 107 | } 108 | 109 | func (q *Queries) GetAssetDetailsByUrl(ctx context.Context, urlPath string) (GetAssetDetailsByUrlRow, error) { 110 | row := q.db.QueryRowContext(ctx, getAssetDetailsByUrl, urlPath) 111 | var i GetAssetDetailsByUrlRow 112 | err := row.Scan(&i.ID, &i.Name, &i.CreatorID) 113 | return i, err 114 | } 115 | 116 | const getFile = `-- name: GetFile :one 117 | SELECT data, mimetype 118 | FROM files f 119 | WHERE f.id = ( 120 | SELECT file_id 121 | FROM assets a 122 | WHERE a.url_path = $1 123 | ) 124 | ` 125 | 126 | type GetFileRow struct { 127 | Data []byte `json:"data"` 128 | Mimetype string `json:"mimetype"` 129 | } 130 | 131 | func (q *Queries) GetFile(ctx context.Context, urlPath string) (GetFileRow, error) { 132 | row := q.db.QueryRowContext(ctx, getFile, urlPath) 133 | var i GetFileRow 134 | err := row.Scan(&i.Data, &i.Mimetype) 135 | return i, err 136 | } 137 | 138 | const listAssetByCreator = `-- name: ListAssetByCreator :many 139 | SELECT id, name, url_path, file_id, creator_id 140 | FROM assets 141 | WHERE creator_id = $1 142 | ORDER BY id 143 | LIMIT $2 -- PageSize 144 | OFFSET $3 145 | ` 146 | 147 | type ListAssetByCreatorParams struct { 148 | CreatorID int64 `json:"creatorID"` 149 | Limit int32 `json:"limit"` 150 | Offset int32 `json:"offset"` 151 | } 152 | 153 | func (q *Queries) ListAssetByCreator(ctx context.Context, arg ListAssetByCreatorParams) ([]Assets, error) { 154 | rows, err := q.db.QueryContext(ctx, listAssetByCreator, arg.CreatorID, arg.Limit, arg.Offset) 155 | if err != nil { 156 | return nil, err 157 | } 158 | defer rows.Close() 159 | var items []Assets 160 | for rows.Next() { 161 | var i Assets 162 | if err := rows.Scan( 163 | &i.ID, 164 | &i.Name, 165 | &i.UrlPath, 166 | &i.FileID, 167 | &i.CreatorID, 168 | ); err != nil { 169 | return nil, err 170 | } 171 | items = append(items, i) 172 | } 173 | if err := rows.Close(); err != nil { 174 | return nil, err 175 | } 176 | if err := rows.Err(); err != nil { 177 | return nil, err 178 | } 179 | return items, nil 180 | } 181 | -------------------------------------------------------------------------------- /db/sqlc/cdn_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "testing" 7 | 8 | "github.com/Tech-With-Tim/cdn/utils" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func createRandomAsset(t *testing.T) (CreateAssetParams, int64) { 14 | arg := CreateAssetParams{ 15 | Mimetype: "application/octet-stream", 16 | Name: utils.RandomString(4) + ".bin", //FileName 17 | Data: utils.StrToBinary(utils.RandomString(16), 10), 18 | Name_2: utils.RandomString(4), //AssetName 19 | UrlPath: utils.RandomString(4), 20 | CreatorID: 735376244656308274, 21 | } 22 | //Context.Background() is to provide empty Context For tests 23 | assetId, err := testQueries.CreateAsset(context.Background(), arg) 24 | require.NoError(t, err) 25 | require.NotEmpty(t, assetId) 26 | require.NotZero(t, assetId) 27 | return arg, assetId 28 | } 29 | 30 | func cleanup(t *testing.T, asset CreateAssetParams) { 31 | deleteArgs := DeleteAssetParams{ 32 | UrlPath: asset.UrlPath, 33 | CreatorID: asset.CreatorID, 34 | } 35 | err := testQueries.DeleteAsset(context.Background(), deleteArgs) 36 | require.NoError(t, err) 37 | } 38 | 39 | func TestQueries_CreateAsset(t *testing.T) { 40 | store := NewStore(testDB) //testDb is a global var check cdn_test.go 41 | // run 6 concurrent transactions 42 | n := 6 43 | errors := make(chan error) 44 | results := make(chan int64) 45 | 46 | for i := 0; i < n; i++ { 47 | go func() { 48 | result, err := store.CreateAssetFile(context.Background(), CreateAssetParams{ 49 | Mimetype: "application/octet-stream", 50 | Name: utils.RandomString(4) + ".bin", //FileName 51 | Data: utils.StrToBinary(utils.RandomString(16), 10), 52 | Name_2: utils.RandomString(4), //AssetName 53 | UrlPath: utils.RandomString(4), 54 | CreatorID: 735376244656308274, 55 | }) 56 | errors <- err 57 | results <- result 58 | 59 | }() 60 | } 61 | 62 | // check results 63 | for i := 0; i < n; i++ { 64 | err := <-errors 65 | require.NoError(t, err) 66 | result := <-results 67 | require.NotEmpty(t, result) 68 | require.NotZero(t, result) 69 | 70 | asset, err := store.GetAssetDetailsById(context.Background(), result) 71 | require.NoError(t, err) 72 | deleteArgs := DeleteAssetParams{ 73 | UrlPath: asset.UrlPath, 74 | CreatorID: asset.CreatorID, 75 | } 76 | err = testQueries.DeleteAsset(context.Background(), deleteArgs) 77 | require.NoError(t, err) 78 | 79 | } 80 | //_, _ = createRandomAsset(t) 81 | } 82 | 83 | func TestQueries_GetAssetDetails(t *testing.T) { 84 | generatedAsset, assetID := createRandomAsset(t) 85 | assetTest, err := testQueries.GetAssetDetailsByUrl(context.Background(), generatedAsset.UrlPath) 86 | require.NoError(t, err) 87 | require.NotEmpty(t, assetTest) 88 | require.Equal(t, assetID, assetTest.ID) 89 | require.Equal(t, generatedAsset.Name_2, assetTest.Name) 90 | assetTestById, err := testQueries.GetAssetDetailsById(context.Background(), assetID) 91 | require.NoError(t, err) 92 | require.NotEmpty(t, assetTestById) 93 | require.Equal(t, assetTest.Name, assetTestById.Name) 94 | cleanup(t, generatedAsset) 95 | } 96 | 97 | func TestQueries_GetFile(t *testing.T) { 98 | generatedAsset, _ := createRandomAsset(t) 99 | fileTest, err := testQueries.GetFile(context.Background(), 100 | generatedAsset.UrlPath) 101 | require.NoError(t, err) 102 | require.NotEmpty(t, fileTest) 103 | require.Equal(t, generatedAsset.Mimetype, fileTest.Mimetype) 104 | require.Equal(t, generatedAsset.Data, fileTest.Data) 105 | 106 | cleanup(t, generatedAsset) 107 | } 108 | 109 | func TestQueries_ListAssetByCreator(t *testing.T) { 110 | generatedAsset, assetID := createRandomAsset(t) 111 | //ExpectedRow := ListAssetByCreatorRow{ 112 | // ID: assetID, 113 | // Name: generatedAsset.Name_2, 114 | // UrlPath: generatedAsset.UrlPath, 115 | //} 116 | var ExpectedRow []Assets 117 | ExpectedRow = append(ExpectedRow, Assets{ 118 | ID: assetID, 119 | Name: generatedAsset.Name_2, 120 | UrlPath: generatedAsset.UrlPath, 121 | FileID: 0, 122 | CreatorID: generatedAsset.CreatorID, 123 | }) 124 | var pageSize int32 = 5 125 | var pageNumber int32 = 1 126 | args := ListAssetByCreatorParams{ 127 | CreatorID: generatedAsset.CreatorID, 128 | Limit: 5, 129 | Offset: (pageNumber - 1) * pageSize, 130 | } 131 | assetLists, err := testQueries.ListAssetByCreator(context.Background(), args) 132 | require.NoError(t, err) 133 | require.NotEmpty(t, assetLists) 134 | returnedAsset := assetLists[0] 135 | ExpectedRow[0].FileID = returnedAsset.FileID 136 | require.Equal(t, ExpectedRow, assetLists) 137 | cleanup(t, generatedAsset) 138 | } 139 | 140 | func TestQueries_DeleteAsset(t *testing.T) { 141 | generatedAsset, _ := createRandomAsset(t) 142 | args := DeleteAssetParams{ 143 | UrlPath: generatedAsset.UrlPath, 144 | CreatorID: generatedAsset.CreatorID, 145 | } 146 | err := testQueries.DeleteAsset(context.Background(), args) 147 | require.NoError(t, err) 148 | asset2, err := testQueries.GetAssetDetailsByUrl(context.Background(), generatedAsset.UrlPath) 149 | require.Error(t, err) 150 | require.EqualError(t, err, sql.ErrNoRows.Error()) 151 | require.Empty(t, asset2) 152 | } 153 | -------------------------------------------------------------------------------- /db/sqlc/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | 3 | package db 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | ) 9 | 10 | type DBTX interface { 11 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 12 | PrepareContext(context.Context, string) (*sql.Stmt, error) 13 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 14 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 15 | } 16 | 17 | func New(db DBTX) *Queries { 18 | return &Queries{db: db} 19 | } 20 | 21 | type Queries struct { 22 | db DBTX 23 | } 24 | 25 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 26 | return &Queries{ 27 | db: tx, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /db/sqlc/main_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/Tech-With-Tim/cdn/utils" 11 | _ "github.com/lib/pq" 12 | ) 13 | 14 | var testQueries *Queries 15 | var testDB *sql.DB 16 | 17 | func TestMain(m *testing.M) { 18 | config, err := utils.LoadConfig("../../", "test") 19 | if err != nil { 20 | log.Fatalln(err.Error()) 21 | } 22 | dbSource := config.DBUri 23 | testDB, err = sql.Open("postgres", dbSource) 24 | if err != nil { 25 | log.Fatalln(err.Error()) 26 | } 27 | testQueries = New(testDB) 28 | err = createTestUser() 29 | if err != nil { 30 | log.Fatalf("error: %v", err.Error()) 31 | } 32 | os.Exit(m.Run()) 33 | } 34 | 35 | func createTestUser() error { 36 | user := CreateUserParams{ 37 | ID: 735376244656308274, 38 | Username: utils.RandomString(5), 39 | Discriminator: "4876", 40 | } 41 | err := testQueries.CreateUser(context.Background(), user) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /db/sqlc/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | 3 | package db 4 | 5 | import ( 6 | "database/sql" 7 | "encoding/json" 8 | "time" 9 | ) 10 | 11 | type Assets struct { 12 | ID int64 `json:"id"` 13 | Name string `json:"name"` 14 | UrlPath string `json:"urlPath"` 15 | FileID int64 `json:"fileID"` 16 | CreatorID int64 `json:"creatorID"` 17 | } 18 | 19 | type Challenges struct { 20 | ID int64 `json:"id"` 21 | Title string `json:"title"` 22 | AuthorID int64 `json:"authorID"` 23 | Description string `json:"description"` 24 | Rules string `json:"rules"` 25 | Reward int32 `json:"reward"` 26 | } 27 | 28 | type Challengesubmissions struct { 29 | ID int64 `json:"id"` 30 | ChallengeID int64 `json:"challengeID"` 31 | Code string `json:"code"` 32 | Language string `json:"language"` 33 | AuthorID int64 `json:"authorID"` 34 | } 35 | 36 | type Files struct { 37 | ID int64 `json:"id"` 38 | Name string `json:"name"` 39 | Mimetype string `json:"mimetype"` 40 | Data []byte `json:"data"` 41 | } 42 | 43 | type Tokens struct { 44 | UserID int64 `json:"userID"` 45 | ExpiresAt time.Time `json:"expiresAt"` 46 | Token string `json:"token"` 47 | Data json.RawMessage `json:"data"` 48 | } 49 | 50 | type Users struct { 51 | ID int64 `json:"id"` 52 | Username string `json:"username"` 53 | Discriminator string `json:"discriminator"` 54 | Avatar sql.NullString `json:"avatar"` 55 | App bool `json:"app"` 56 | } 57 | -------------------------------------------------------------------------------- /db/sqlc/store.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | ) 8 | 9 | // Store provides all functions to execute db queries and transactions 10 | type Store struct { 11 | *Queries // Embedding Queries struct generated by sqlc 12 | db *sql.DB 13 | } 14 | 15 | // NewStore Creates a New Store 16 | func NewStore(db *sql.DB) *Store { 17 | return &Store{ 18 | db: db, 19 | Queries: New(db), 20 | } 21 | } 22 | 23 | // execTx executes a function in a db transaction 24 | func (store *Store) execTx(ctx context.Context, fn func(*Queries) error) error { 25 | tx, err := store.db.BeginTx(ctx, nil) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | q := New(tx) 31 | err = fn(q) 32 | if err != nil { 33 | if rbErr := tx.Rollback(); rbErr != nil { //Trying To rollback returns error as well 34 | return fmt.Errorf("transaction error: %v, rollback Error: %v", 35 | err, rbErr) 36 | } 37 | return err 38 | } 39 | return tx.Commit() 40 | } 41 | 42 | // CreateAssetFile Creates assets and files in a transaction so that errors can be caught 43 | // before committing 44 | func (store *Store) CreateAssetFile(ctx context.Context, arg CreateAssetParams) (int64, error) { 45 | var result int64 46 | 47 | err := store.execTx(ctx, func(q *Queries) error { 48 | var err error 49 | result, err = q.CreateAsset(ctx, arg) 50 | if err != nil { 51 | return err 52 | } 53 | return nil 54 | }) 55 | return result, err 56 | } 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | postgres: 4 | image: postgres:12-alpine 5 | restart: always 6 | environment: 7 | - POSTGRES_USER=twt 8 | - POSTGRES_PASSWORD=twt 9 | - POSTGRES_DB=twt 10 | 11 | redis: 12 | image: redis:6.2 13 | restart: always 14 | 15 | cdn: 16 | build: 17 | context: . 18 | dockerfile: Dockerfile 19 | command: > 20 | sh -c "/app/main migrate_up | true; /app/main runserver --host 0.0.0.0" 21 | ports: 22 | - "5000:5000" 23 | environment: 24 | - DB_URI=postgres://twt:twt@postgres:5432/twt?sslmode=disable 25 | - SECRET_KEY=mysecret # make sure to keep the same secret key in the jwt issuer 26 | - MAX_FILE_SIZE=30 27 | - REDIS_HOST=redis:6379 28 | - REDIS_DB=0 29 | depends_on: 30 | - postgres 31 | - redis 32 | links: 33 | - postgres 34 | - redis 35 | 36 | -------------------------------------------------------------------------------- /docs/docs-template/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CDN Docs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "CDN Docs", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "sirv-cli": "^1.0.0" 12 | }, 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^17.0.0", 15 | "@rollup/plugin-node-resolve": "^11.0.0", 16 | "rollup": "^2.3.4", 17 | "rollup-plugin-css-only": "^3.1.0", 18 | "rollup-plugin-livereload": "^2.0.0", 19 | "rollup-plugin-svelte": "^7.0.0", 20 | "rollup-plugin-terser": "^7.0.0", 21 | "svelte": "^3.0.0" 22 | } 23 | }, 24 | "node_modules/@babel/code-frame": { 25 | "version": "7.14.5", 26 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", 27 | "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", 28 | "dev": true, 29 | "dependencies": { 30 | "@babel/highlight": "^7.14.5" 31 | }, 32 | "engines": { 33 | "node": ">=6.9.0" 34 | } 35 | }, 36 | "node_modules/@babel/helper-validator-identifier": { 37 | "version": "7.14.5", 38 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", 39 | "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", 40 | "dev": true, 41 | "engines": { 42 | "node": ">=6.9.0" 43 | } 44 | }, 45 | "node_modules/@babel/highlight": { 46 | "version": "7.14.5", 47 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", 48 | "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", 49 | "dev": true, 50 | "dependencies": { 51 | "@babel/helper-validator-identifier": "^7.14.5", 52 | "chalk": "^2.0.0", 53 | "js-tokens": "^4.0.0" 54 | }, 55 | "engines": { 56 | "node": ">=6.9.0" 57 | } 58 | }, 59 | "node_modules/@polka/url": { 60 | "version": "1.0.0-next.15", 61 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz", 62 | "integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA==" 63 | }, 64 | "node_modules/@rollup/plugin-commonjs": { 65 | "version": "17.1.0", 66 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", 67 | "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", 68 | "dev": true, 69 | "dependencies": { 70 | "@rollup/pluginutils": "^3.1.0", 71 | "commondir": "^1.0.1", 72 | "estree-walker": "^2.0.1", 73 | "glob": "^7.1.6", 74 | "is-reference": "^1.2.1", 75 | "magic-string": "^0.25.7", 76 | "resolve": "^1.17.0" 77 | }, 78 | "engines": { 79 | "node": ">= 8.0.0" 80 | }, 81 | "peerDependencies": { 82 | "rollup": "^2.30.0" 83 | } 84 | }, 85 | "node_modules/@rollup/plugin-node-resolve": { 86 | "version": "11.2.1", 87 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", 88 | "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", 89 | "dev": true, 90 | "dependencies": { 91 | "@rollup/pluginutils": "^3.1.0", 92 | "@types/resolve": "1.17.1", 93 | "builtin-modules": "^3.1.0", 94 | "deepmerge": "^4.2.2", 95 | "is-module": "^1.0.0", 96 | "resolve": "^1.19.0" 97 | }, 98 | "engines": { 99 | "node": ">= 10.0.0" 100 | }, 101 | "peerDependencies": { 102 | "rollup": "^1.20.0||^2.0.0" 103 | } 104 | }, 105 | "node_modules/@rollup/pluginutils": { 106 | "version": "3.1.0", 107 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", 108 | "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", 109 | "dev": true, 110 | "dependencies": { 111 | "@types/estree": "0.0.39", 112 | "estree-walker": "^1.0.1", 113 | "picomatch": "^2.2.2" 114 | }, 115 | "engines": { 116 | "node": ">= 8.0.0" 117 | }, 118 | "peerDependencies": { 119 | "rollup": "^1.20.0||^2.0.0" 120 | } 121 | }, 122 | "node_modules/@rollup/pluginutils/node_modules/estree-walker": { 123 | "version": "1.0.1", 124 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 125 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 126 | "dev": true 127 | }, 128 | "node_modules/@types/estree": { 129 | "version": "0.0.39", 130 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 131 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 132 | "dev": true 133 | }, 134 | "node_modules/@types/node": { 135 | "version": "16.0.1", 136 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.0.1.tgz", 137 | "integrity": "sha512-hBOx4SUlEPKwRi6PrXuTGw1z6lz0fjsibcWCM378YxsSu/6+C30L6CR49zIBKHiwNWCYIcOLjg4OHKZaFeLAug==", 138 | "dev": true 139 | }, 140 | "node_modules/@types/resolve": { 141 | "version": "1.17.1", 142 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", 143 | "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", 144 | "dev": true, 145 | "dependencies": { 146 | "@types/node": "*" 147 | } 148 | }, 149 | "node_modules/ansi-styles": { 150 | "version": "3.2.1", 151 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 152 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 153 | "dev": true, 154 | "dependencies": { 155 | "color-convert": "^1.9.0" 156 | }, 157 | "engines": { 158 | "node": ">=4" 159 | } 160 | }, 161 | "node_modules/anymatch": { 162 | "version": "3.1.2", 163 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 164 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 165 | "dev": true, 166 | "dependencies": { 167 | "normalize-path": "^3.0.0", 168 | "picomatch": "^2.0.4" 169 | }, 170 | "engines": { 171 | "node": ">= 8" 172 | } 173 | }, 174 | "node_modules/balanced-match": { 175 | "version": "1.0.2", 176 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 177 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 178 | "dev": true 179 | }, 180 | "node_modules/binary-extensions": { 181 | "version": "2.2.0", 182 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 183 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 184 | "dev": true, 185 | "engines": { 186 | "node": ">=8" 187 | } 188 | }, 189 | "node_modules/brace-expansion": { 190 | "version": "1.1.11", 191 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 192 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 193 | "dev": true, 194 | "dependencies": { 195 | "balanced-match": "^1.0.0", 196 | "concat-map": "0.0.1" 197 | } 198 | }, 199 | "node_modules/braces": { 200 | "version": "3.0.2", 201 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 202 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 203 | "dev": true, 204 | "dependencies": { 205 | "fill-range": "^7.0.1" 206 | }, 207 | "engines": { 208 | "node": ">=8" 209 | } 210 | }, 211 | "node_modules/buffer-from": { 212 | "version": "1.1.1", 213 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 214 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 215 | "dev": true 216 | }, 217 | "node_modules/builtin-modules": { 218 | "version": "3.2.0", 219 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", 220 | "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", 221 | "dev": true, 222 | "engines": { 223 | "node": ">=6" 224 | }, 225 | "funding": { 226 | "url": "https://github.com/sponsors/sindresorhus" 227 | } 228 | }, 229 | "node_modules/chalk": { 230 | "version": "2.4.2", 231 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 232 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 233 | "dev": true, 234 | "dependencies": { 235 | "ansi-styles": "^3.2.1", 236 | "escape-string-regexp": "^1.0.5", 237 | "supports-color": "^5.3.0" 238 | }, 239 | "engines": { 240 | "node": ">=4" 241 | } 242 | }, 243 | "node_modules/chokidar": { 244 | "version": "3.5.2", 245 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 246 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 247 | "dev": true, 248 | "dependencies": { 249 | "anymatch": "~3.1.2", 250 | "braces": "~3.0.2", 251 | "glob-parent": "~5.1.2", 252 | "is-binary-path": "~2.1.0", 253 | "is-glob": "~4.0.1", 254 | "normalize-path": "~3.0.0", 255 | "readdirp": "~3.6.0" 256 | }, 257 | "engines": { 258 | "node": ">= 8.10.0" 259 | }, 260 | "optionalDependencies": { 261 | "fsevents": "~2.3.2" 262 | } 263 | }, 264 | "node_modules/color-convert": { 265 | "version": "1.9.3", 266 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 267 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 268 | "dev": true, 269 | "dependencies": { 270 | "color-name": "1.1.3" 271 | } 272 | }, 273 | "node_modules/color-name": { 274 | "version": "1.1.3", 275 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 276 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 277 | "dev": true 278 | }, 279 | "node_modules/commander": { 280 | "version": "2.20.3", 281 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 282 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 283 | "dev": true 284 | }, 285 | "node_modules/commondir": { 286 | "version": "1.0.1", 287 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 288 | "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", 289 | "dev": true 290 | }, 291 | "node_modules/concat-map": { 292 | "version": "0.0.1", 293 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 294 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 295 | "dev": true 296 | }, 297 | "node_modules/console-clear": { 298 | "version": "1.1.1", 299 | "resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz", 300 | "integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==", 301 | "engines": { 302 | "node": ">=4" 303 | } 304 | }, 305 | "node_modules/deepmerge": { 306 | "version": "4.2.2", 307 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", 308 | "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", 309 | "dev": true, 310 | "engines": { 311 | "node": ">=0.10.0" 312 | } 313 | }, 314 | "node_modules/escape-string-regexp": { 315 | "version": "1.0.5", 316 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 317 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 318 | "dev": true, 319 | "engines": { 320 | "node": ">=0.8.0" 321 | } 322 | }, 323 | "node_modules/estree-walker": { 324 | "version": "2.0.2", 325 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 326 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 327 | "dev": true 328 | }, 329 | "node_modules/fill-range": { 330 | "version": "7.0.1", 331 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 332 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 333 | "dev": true, 334 | "dependencies": { 335 | "to-regex-range": "^5.0.1" 336 | }, 337 | "engines": { 338 | "node": ">=8" 339 | } 340 | }, 341 | "node_modules/fs.realpath": { 342 | "version": "1.0.0", 343 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 344 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 345 | "dev": true 346 | }, 347 | "node_modules/fsevents": { 348 | "version": "2.3.2", 349 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 350 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 351 | "dev": true, 352 | "hasInstallScript": true, 353 | "optional": true, 354 | "os": [ 355 | "darwin" 356 | ], 357 | "engines": { 358 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 359 | } 360 | }, 361 | "node_modules/function-bind": { 362 | "version": "1.1.1", 363 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 364 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 365 | "dev": true 366 | }, 367 | "node_modules/get-port": { 368 | "version": "3.2.0", 369 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", 370 | "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", 371 | "engines": { 372 | "node": ">=4" 373 | } 374 | }, 375 | "node_modules/glob": { 376 | "version": "7.1.7", 377 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 378 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 379 | "dev": true, 380 | "dependencies": { 381 | "fs.realpath": "^1.0.0", 382 | "inflight": "^1.0.4", 383 | "inherits": "2", 384 | "minimatch": "^3.0.4", 385 | "once": "^1.3.0", 386 | "path-is-absolute": "^1.0.0" 387 | }, 388 | "engines": { 389 | "node": "*" 390 | }, 391 | "funding": { 392 | "url": "https://github.com/sponsors/isaacs" 393 | } 394 | }, 395 | "node_modules/glob-parent": { 396 | "version": "5.1.2", 397 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 398 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 399 | "dev": true, 400 | "dependencies": { 401 | "is-glob": "^4.0.1" 402 | }, 403 | "engines": { 404 | "node": ">= 6" 405 | } 406 | }, 407 | "node_modules/has": { 408 | "version": "1.0.3", 409 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 410 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 411 | "dev": true, 412 | "dependencies": { 413 | "function-bind": "^1.1.1" 414 | }, 415 | "engines": { 416 | "node": ">= 0.4.0" 417 | } 418 | }, 419 | "node_modules/has-flag": { 420 | "version": "3.0.0", 421 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 422 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 423 | "dev": true, 424 | "engines": { 425 | "node": ">=4" 426 | } 427 | }, 428 | "node_modules/inflight": { 429 | "version": "1.0.6", 430 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 431 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 432 | "dev": true, 433 | "dependencies": { 434 | "once": "^1.3.0", 435 | "wrappy": "1" 436 | } 437 | }, 438 | "node_modules/inherits": { 439 | "version": "2.0.4", 440 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 441 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 442 | "dev": true 443 | }, 444 | "node_modules/is-binary-path": { 445 | "version": "2.1.0", 446 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 447 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 448 | "dev": true, 449 | "dependencies": { 450 | "binary-extensions": "^2.0.0" 451 | }, 452 | "engines": { 453 | "node": ">=8" 454 | } 455 | }, 456 | "node_modules/is-core-module": { 457 | "version": "2.4.0", 458 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", 459 | "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", 460 | "dev": true, 461 | "dependencies": { 462 | "has": "^1.0.3" 463 | }, 464 | "funding": { 465 | "url": "https://github.com/sponsors/ljharb" 466 | } 467 | }, 468 | "node_modules/is-extglob": { 469 | "version": "2.1.1", 470 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 471 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 472 | "dev": true, 473 | "engines": { 474 | "node": ">=0.10.0" 475 | } 476 | }, 477 | "node_modules/is-glob": { 478 | "version": "4.0.1", 479 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 480 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 481 | "dev": true, 482 | "dependencies": { 483 | "is-extglob": "^2.1.1" 484 | }, 485 | "engines": { 486 | "node": ">=0.10.0" 487 | } 488 | }, 489 | "node_modules/is-module": { 490 | "version": "1.0.0", 491 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 492 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 493 | "dev": true 494 | }, 495 | "node_modules/is-number": { 496 | "version": "7.0.0", 497 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 498 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 499 | "dev": true, 500 | "engines": { 501 | "node": ">=0.12.0" 502 | } 503 | }, 504 | "node_modules/is-reference": { 505 | "version": "1.2.1", 506 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", 507 | "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", 508 | "dev": true, 509 | "dependencies": { 510 | "@types/estree": "*" 511 | } 512 | }, 513 | "node_modules/jest-worker": { 514 | "version": "26.6.2", 515 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", 516 | "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", 517 | "dev": true, 518 | "dependencies": { 519 | "@types/node": "*", 520 | "merge-stream": "^2.0.0", 521 | "supports-color": "^7.0.0" 522 | }, 523 | "engines": { 524 | "node": ">= 10.13.0" 525 | } 526 | }, 527 | "node_modules/jest-worker/node_modules/has-flag": { 528 | "version": "4.0.0", 529 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 530 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 531 | "dev": true, 532 | "engines": { 533 | "node": ">=8" 534 | } 535 | }, 536 | "node_modules/jest-worker/node_modules/supports-color": { 537 | "version": "7.2.0", 538 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 539 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 540 | "dev": true, 541 | "dependencies": { 542 | "has-flag": "^4.0.0" 543 | }, 544 | "engines": { 545 | "node": ">=8" 546 | } 547 | }, 548 | "node_modules/js-tokens": { 549 | "version": "4.0.0", 550 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 551 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 552 | "dev": true 553 | }, 554 | "node_modules/kleur": { 555 | "version": "3.0.3", 556 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 557 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", 558 | "engines": { 559 | "node": ">=6" 560 | } 561 | }, 562 | "node_modules/livereload": { 563 | "version": "0.9.3", 564 | "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", 565 | "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", 566 | "dev": true, 567 | "dependencies": { 568 | "chokidar": "^3.5.0", 569 | "livereload-js": "^3.3.1", 570 | "opts": ">= 1.2.0", 571 | "ws": "^7.4.3" 572 | }, 573 | "bin": { 574 | "livereload": "bin/livereload.js" 575 | }, 576 | "engines": { 577 | "node": ">=8.0.0" 578 | } 579 | }, 580 | "node_modules/livereload-js": { 581 | "version": "3.3.2", 582 | "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz", 583 | "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", 584 | "dev": true 585 | }, 586 | "node_modules/local-access": { 587 | "version": "1.1.0", 588 | "resolved": "https://registry.npmjs.org/local-access/-/local-access-1.1.0.tgz", 589 | "integrity": "sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==", 590 | "engines": { 591 | "node": ">=6" 592 | } 593 | }, 594 | "node_modules/magic-string": { 595 | "version": "0.25.7", 596 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 597 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 598 | "dev": true, 599 | "dependencies": { 600 | "sourcemap-codec": "^1.4.4" 601 | } 602 | }, 603 | "node_modules/merge-stream": { 604 | "version": "2.0.0", 605 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 606 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 607 | "dev": true 608 | }, 609 | "node_modules/mime": { 610 | "version": "2.5.2", 611 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", 612 | "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", 613 | "bin": { 614 | "mime": "cli.js" 615 | }, 616 | "engines": { 617 | "node": ">=4.0.0" 618 | } 619 | }, 620 | "node_modules/minimatch": { 621 | "version": "3.0.4", 622 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 623 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 624 | "dev": true, 625 | "dependencies": { 626 | "brace-expansion": "^1.1.7" 627 | }, 628 | "engines": { 629 | "node": "*" 630 | } 631 | }, 632 | "node_modules/mri": { 633 | "version": "1.1.6", 634 | "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz", 635 | "integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==", 636 | "engines": { 637 | "node": ">=4" 638 | } 639 | }, 640 | "node_modules/normalize-path": { 641 | "version": "3.0.0", 642 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 643 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 644 | "dev": true, 645 | "engines": { 646 | "node": ">=0.10.0" 647 | } 648 | }, 649 | "node_modules/once": { 650 | "version": "1.4.0", 651 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 652 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 653 | "dev": true, 654 | "dependencies": { 655 | "wrappy": "1" 656 | } 657 | }, 658 | "node_modules/opts": { 659 | "version": "2.0.2", 660 | "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", 661 | "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", 662 | "dev": true 663 | }, 664 | "node_modules/path-is-absolute": { 665 | "version": "1.0.1", 666 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 667 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 668 | "dev": true, 669 | "engines": { 670 | "node": ">=0.10.0" 671 | } 672 | }, 673 | "node_modules/path-parse": { 674 | "version": "1.0.7", 675 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 676 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 677 | "dev": true 678 | }, 679 | "node_modules/picomatch": { 680 | "version": "2.3.0", 681 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 682 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", 683 | "dev": true, 684 | "engines": { 685 | "node": ">=8.6" 686 | }, 687 | "funding": { 688 | "url": "https://github.com/sponsors/jonschlinkert" 689 | } 690 | }, 691 | "node_modules/randombytes": { 692 | "version": "2.1.0", 693 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 694 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 695 | "dev": true, 696 | "dependencies": { 697 | "safe-buffer": "^5.1.0" 698 | } 699 | }, 700 | "node_modules/readdirp": { 701 | "version": "3.6.0", 702 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 703 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 704 | "dev": true, 705 | "dependencies": { 706 | "picomatch": "^2.2.1" 707 | }, 708 | "engines": { 709 | "node": ">=8.10.0" 710 | } 711 | }, 712 | "node_modules/require-relative": { 713 | "version": "0.8.7", 714 | "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", 715 | "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", 716 | "dev": true 717 | }, 718 | "node_modules/resolve": { 719 | "version": "1.20.0", 720 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 721 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 722 | "dev": true, 723 | "dependencies": { 724 | "is-core-module": "^2.2.0", 725 | "path-parse": "^1.0.6" 726 | }, 727 | "funding": { 728 | "url": "https://github.com/sponsors/ljharb" 729 | } 730 | }, 731 | "node_modules/rollup": { 732 | "version": "2.52.8", 733 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.8.tgz", 734 | "integrity": "sha512-IjAB0C6KK5/lvqzJWAzsvOik+jV5Bt907QdkQ/gDP4j+R9KYNI1tjqdxiPitGPVrWC21Mf/ucXgowUjN/VemaQ==", 735 | "dev": true, 736 | "bin": { 737 | "rollup": "dist/bin/rollup" 738 | }, 739 | "engines": { 740 | "node": ">=10.0.0" 741 | }, 742 | "optionalDependencies": { 743 | "fsevents": "~2.3.2" 744 | } 745 | }, 746 | "node_modules/rollup-plugin-css-only": { 747 | "version": "3.1.0", 748 | "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz", 749 | "integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==", 750 | "dev": true, 751 | "dependencies": { 752 | "@rollup/pluginutils": "4" 753 | }, 754 | "engines": { 755 | "node": ">=10.12.0" 756 | }, 757 | "peerDependencies": { 758 | "rollup": "1 || 2" 759 | } 760 | }, 761 | "node_modules/rollup-plugin-css-only/node_modules/@rollup/pluginutils": { 762 | "version": "4.1.0", 763 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz", 764 | "integrity": "sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ==", 765 | "dev": true, 766 | "dependencies": { 767 | "estree-walker": "^2.0.1", 768 | "picomatch": "^2.2.2" 769 | }, 770 | "engines": { 771 | "node": ">= 8.0.0" 772 | }, 773 | "peerDependencies": { 774 | "rollup": "^1.20.0||^2.0.0" 775 | } 776 | }, 777 | "node_modules/rollup-plugin-livereload": { 778 | "version": "2.0.5", 779 | "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz", 780 | "integrity": "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==", 781 | "dev": true, 782 | "dependencies": { 783 | "livereload": "^0.9.1" 784 | }, 785 | "engines": { 786 | "node": ">=8.3" 787 | } 788 | }, 789 | "node_modules/rollup-plugin-svelte": { 790 | "version": "7.1.0", 791 | "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", 792 | "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", 793 | "dev": true, 794 | "dependencies": { 795 | "require-relative": "^0.8.7", 796 | "rollup-pluginutils": "^2.8.2" 797 | }, 798 | "engines": { 799 | "node": ">=10" 800 | }, 801 | "peerDependencies": { 802 | "rollup": ">=2.0.0", 803 | "svelte": ">=3.5.0" 804 | } 805 | }, 806 | "node_modules/rollup-plugin-terser": { 807 | "version": "7.0.2", 808 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", 809 | "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", 810 | "dev": true, 811 | "dependencies": { 812 | "@babel/code-frame": "^7.10.4", 813 | "jest-worker": "^26.2.1", 814 | "serialize-javascript": "^4.0.0", 815 | "terser": "^5.0.0" 816 | }, 817 | "peerDependencies": { 818 | "rollup": "^2.0.0" 819 | } 820 | }, 821 | "node_modules/rollup-pluginutils": { 822 | "version": "2.8.2", 823 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 824 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 825 | "dev": true, 826 | "dependencies": { 827 | "estree-walker": "^0.6.1" 828 | } 829 | }, 830 | "node_modules/rollup-pluginutils/node_modules/estree-walker": { 831 | "version": "0.6.1", 832 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 833 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 834 | "dev": true 835 | }, 836 | "node_modules/sade": { 837 | "version": "1.7.4", 838 | "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz", 839 | "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==", 840 | "dependencies": { 841 | "mri": "^1.1.0" 842 | }, 843 | "engines": { 844 | "node": ">= 6" 845 | } 846 | }, 847 | "node_modules/safe-buffer": { 848 | "version": "5.2.1", 849 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 850 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 851 | "dev": true, 852 | "funding": [ 853 | { 854 | "type": "github", 855 | "url": "https://github.com/sponsors/feross" 856 | }, 857 | { 858 | "type": "patreon", 859 | "url": "https://www.patreon.com/feross" 860 | }, 861 | { 862 | "type": "consulting", 863 | "url": "https://feross.org/support" 864 | } 865 | ] 866 | }, 867 | "node_modules/semiver": { 868 | "version": "1.1.0", 869 | "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", 870 | "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==", 871 | "engines": { 872 | "node": ">=6" 873 | } 874 | }, 875 | "node_modules/serialize-javascript": { 876 | "version": "4.0.0", 877 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", 878 | "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", 879 | "dev": true, 880 | "dependencies": { 881 | "randombytes": "^2.1.0" 882 | } 883 | }, 884 | "node_modules/sirv": { 885 | "version": "1.0.12", 886 | "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.12.tgz", 887 | "integrity": "sha512-+jQoCxndz7L2tqQL4ZyzfDhky0W/4ZJip3XoOuxyQWnAwMxindLl3Xv1qT4x1YX/re0leShvTm8Uk0kQspGhBg==", 888 | "dependencies": { 889 | "@polka/url": "^1.0.0-next.15", 890 | "mime": "^2.3.1", 891 | "totalist": "^1.0.0" 892 | }, 893 | "engines": { 894 | "node": ">= 10" 895 | } 896 | }, 897 | "node_modules/sirv-cli": { 898 | "version": "1.0.12", 899 | "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-1.0.12.tgz", 900 | "integrity": "sha512-Rs5PvF3a48zuLmrl8vcqVv9xF/WWPES19QawVkpdzqx7vD5SMZS07+ece1gK4umbslXN43YeIksYtQM5csgIzQ==", 901 | "dependencies": { 902 | "console-clear": "^1.1.0", 903 | "get-port": "^3.2.0", 904 | "kleur": "^3.0.0", 905 | "local-access": "^1.0.1", 906 | "sade": "^1.6.0", 907 | "semiver": "^1.0.0", 908 | "sirv": "^1.0.12", 909 | "tinydate": "^1.0.0" 910 | }, 911 | "bin": { 912 | "sirv": "bin.js" 913 | }, 914 | "engines": { 915 | "node": ">= 10" 916 | } 917 | }, 918 | "node_modules/source-map": { 919 | "version": "0.7.3", 920 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 921 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 922 | "dev": true, 923 | "engines": { 924 | "node": ">= 8" 925 | } 926 | }, 927 | "node_modules/source-map-support": { 928 | "version": "0.5.19", 929 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 930 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 931 | "dev": true, 932 | "dependencies": { 933 | "buffer-from": "^1.0.0", 934 | "source-map": "^0.6.0" 935 | } 936 | }, 937 | "node_modules/source-map-support/node_modules/source-map": { 938 | "version": "0.6.1", 939 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 940 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 941 | "dev": true, 942 | "engines": { 943 | "node": ">=0.10.0" 944 | } 945 | }, 946 | "node_modules/sourcemap-codec": { 947 | "version": "1.4.8", 948 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 949 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 950 | "dev": true 951 | }, 952 | "node_modules/supports-color": { 953 | "version": "5.5.0", 954 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 955 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 956 | "dev": true, 957 | "dependencies": { 958 | "has-flag": "^3.0.0" 959 | }, 960 | "engines": { 961 | "node": ">=4" 962 | } 963 | }, 964 | "node_modules/svelte": { 965 | "version": "3.38.3", 966 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.38.3.tgz", 967 | "integrity": "sha512-N7bBZJH0iF24wsalFZF+fVYMUOigaAUQMIcEKHO3jstK/iL8VmP9xE+P0/a76+FkNcWt+TDv2Gx1taUoUscrvw==", 968 | "dev": true, 969 | "engines": { 970 | "node": ">= 8" 971 | } 972 | }, 973 | "node_modules/terser": { 974 | "version": "5.7.1", 975 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz", 976 | "integrity": "sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==", 977 | "dev": true, 978 | "dependencies": { 979 | "commander": "^2.20.0", 980 | "source-map": "~0.7.2", 981 | "source-map-support": "~0.5.19" 982 | }, 983 | "bin": { 984 | "terser": "bin/terser" 985 | }, 986 | "engines": { 987 | "node": ">=10" 988 | } 989 | }, 990 | "node_modules/tinydate": { 991 | "version": "1.3.0", 992 | "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", 993 | "integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==", 994 | "engines": { 995 | "node": ">=4" 996 | } 997 | }, 998 | "node_modules/to-regex-range": { 999 | "version": "5.0.1", 1000 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1001 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1002 | "dev": true, 1003 | "dependencies": { 1004 | "is-number": "^7.0.0" 1005 | }, 1006 | "engines": { 1007 | "node": ">=8.0" 1008 | } 1009 | }, 1010 | "node_modules/totalist": { 1011 | "version": "1.1.0", 1012 | "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", 1013 | "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", 1014 | "engines": { 1015 | "node": ">=6" 1016 | } 1017 | }, 1018 | "node_modules/wrappy": { 1019 | "version": "1.0.2", 1020 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1021 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1022 | "dev": true 1023 | }, 1024 | "node_modules/ws": { 1025 | "version": "7.5.2", 1026 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.2.tgz", 1027 | "integrity": "sha512-lkF7AWRicoB9mAgjeKbGqVUekLnSNO4VjKVnuPHpQeOxZOErX6BPXwJk70nFslRCEEA8EVW7ZjKwXaP9N+1sKQ==", 1028 | "dev": true, 1029 | "engines": { 1030 | "node": ">=8.3.0" 1031 | }, 1032 | "peerDependencies": { 1033 | "bufferutil": "^4.0.1", 1034 | "utf-8-validate": "^5.0.2" 1035 | }, 1036 | "peerDependenciesMeta": { 1037 | "bufferutil": { 1038 | "optional": true 1039 | }, 1040 | "utf-8-validate": { 1041 | "optional": true 1042 | } 1043 | } 1044 | } 1045 | }, 1046 | "dependencies": { 1047 | "@babel/code-frame": { 1048 | "version": "7.14.5", 1049 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", 1050 | "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", 1051 | "dev": true, 1052 | "requires": { 1053 | "@babel/highlight": "^7.14.5" 1054 | } 1055 | }, 1056 | "@babel/helper-validator-identifier": { 1057 | "version": "7.14.5", 1058 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", 1059 | "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", 1060 | "dev": true 1061 | }, 1062 | "@babel/highlight": { 1063 | "version": "7.14.5", 1064 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", 1065 | "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", 1066 | "dev": true, 1067 | "requires": { 1068 | "@babel/helper-validator-identifier": "^7.14.5", 1069 | "chalk": "^2.0.0", 1070 | "js-tokens": "^4.0.0" 1071 | } 1072 | }, 1073 | "@polka/url": { 1074 | "version": "1.0.0-next.15", 1075 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz", 1076 | "integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA==" 1077 | }, 1078 | "@rollup/plugin-commonjs": { 1079 | "version": "17.1.0", 1080 | "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", 1081 | "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", 1082 | "dev": true, 1083 | "requires": { 1084 | "@rollup/pluginutils": "^3.1.0", 1085 | "commondir": "^1.0.1", 1086 | "estree-walker": "^2.0.1", 1087 | "glob": "^7.1.6", 1088 | "is-reference": "^1.2.1", 1089 | "magic-string": "^0.25.7", 1090 | "resolve": "^1.17.0" 1091 | } 1092 | }, 1093 | "@rollup/plugin-node-resolve": { 1094 | "version": "11.2.1", 1095 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", 1096 | "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", 1097 | "dev": true, 1098 | "requires": { 1099 | "@rollup/pluginutils": "^3.1.0", 1100 | "@types/resolve": "1.17.1", 1101 | "builtin-modules": "^3.1.0", 1102 | "deepmerge": "^4.2.2", 1103 | "is-module": "^1.0.0", 1104 | "resolve": "^1.19.0" 1105 | } 1106 | }, 1107 | "@rollup/pluginutils": { 1108 | "version": "3.1.0", 1109 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", 1110 | "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", 1111 | "dev": true, 1112 | "requires": { 1113 | "@types/estree": "0.0.39", 1114 | "estree-walker": "^1.0.1", 1115 | "picomatch": "^2.2.2" 1116 | }, 1117 | "dependencies": { 1118 | "estree-walker": { 1119 | "version": "1.0.1", 1120 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 1121 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 1122 | "dev": true 1123 | } 1124 | } 1125 | }, 1126 | "@types/estree": { 1127 | "version": "0.0.39", 1128 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 1129 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 1130 | "dev": true 1131 | }, 1132 | "@types/node": { 1133 | "version": "16.0.1", 1134 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.0.1.tgz", 1135 | "integrity": "sha512-hBOx4SUlEPKwRi6PrXuTGw1z6lz0fjsibcWCM378YxsSu/6+C30L6CR49zIBKHiwNWCYIcOLjg4OHKZaFeLAug==", 1136 | "dev": true 1137 | }, 1138 | "@types/resolve": { 1139 | "version": "1.17.1", 1140 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", 1141 | "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", 1142 | "dev": true, 1143 | "requires": { 1144 | "@types/node": "*" 1145 | } 1146 | }, 1147 | "ansi-styles": { 1148 | "version": "3.2.1", 1149 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 1150 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 1151 | "dev": true, 1152 | "requires": { 1153 | "color-convert": "^1.9.0" 1154 | } 1155 | }, 1156 | "anymatch": { 1157 | "version": "3.1.2", 1158 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 1159 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 1160 | "dev": true, 1161 | "requires": { 1162 | "normalize-path": "^3.0.0", 1163 | "picomatch": "^2.0.4" 1164 | } 1165 | }, 1166 | "balanced-match": { 1167 | "version": "1.0.2", 1168 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1169 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1170 | "dev": true 1171 | }, 1172 | "binary-extensions": { 1173 | "version": "2.2.0", 1174 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 1175 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 1176 | "dev": true 1177 | }, 1178 | "brace-expansion": { 1179 | "version": "1.1.11", 1180 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1181 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1182 | "dev": true, 1183 | "requires": { 1184 | "balanced-match": "^1.0.0", 1185 | "concat-map": "0.0.1" 1186 | } 1187 | }, 1188 | "braces": { 1189 | "version": "3.0.2", 1190 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 1191 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 1192 | "dev": true, 1193 | "requires": { 1194 | "fill-range": "^7.0.1" 1195 | } 1196 | }, 1197 | "buffer-from": { 1198 | "version": "1.1.1", 1199 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 1200 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 1201 | "dev": true 1202 | }, 1203 | "builtin-modules": { 1204 | "version": "3.2.0", 1205 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", 1206 | "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", 1207 | "dev": true 1208 | }, 1209 | "chalk": { 1210 | "version": "2.4.2", 1211 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 1212 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 1213 | "dev": true, 1214 | "requires": { 1215 | "ansi-styles": "^3.2.1", 1216 | "escape-string-regexp": "^1.0.5", 1217 | "supports-color": "^5.3.0" 1218 | } 1219 | }, 1220 | "chokidar": { 1221 | "version": "3.5.2", 1222 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 1223 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 1224 | "dev": true, 1225 | "requires": { 1226 | "anymatch": "~3.1.2", 1227 | "braces": "~3.0.2", 1228 | "fsevents": "~2.3.2", 1229 | "glob-parent": "~5.1.2", 1230 | "is-binary-path": "~2.1.0", 1231 | "is-glob": "~4.0.1", 1232 | "normalize-path": "~3.0.0", 1233 | "readdirp": "~3.6.0" 1234 | } 1235 | }, 1236 | "color-convert": { 1237 | "version": "1.9.3", 1238 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 1239 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 1240 | "dev": true, 1241 | "requires": { 1242 | "color-name": "1.1.3" 1243 | } 1244 | }, 1245 | "color-name": { 1246 | "version": "1.1.3", 1247 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 1248 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 1249 | "dev": true 1250 | }, 1251 | "commander": { 1252 | "version": "2.20.3", 1253 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 1254 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 1255 | "dev": true 1256 | }, 1257 | "commondir": { 1258 | "version": "1.0.1", 1259 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 1260 | "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", 1261 | "dev": true 1262 | }, 1263 | "concat-map": { 1264 | "version": "0.0.1", 1265 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1266 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 1267 | "dev": true 1268 | }, 1269 | "console-clear": { 1270 | "version": "1.1.1", 1271 | "resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz", 1272 | "integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==" 1273 | }, 1274 | "deepmerge": { 1275 | "version": "4.2.2", 1276 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", 1277 | "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", 1278 | "dev": true 1279 | }, 1280 | "escape-string-regexp": { 1281 | "version": "1.0.5", 1282 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 1283 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 1284 | "dev": true 1285 | }, 1286 | "estree-walker": { 1287 | "version": "2.0.2", 1288 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 1289 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 1290 | "dev": true 1291 | }, 1292 | "fill-range": { 1293 | "version": "7.0.1", 1294 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 1295 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 1296 | "dev": true, 1297 | "requires": { 1298 | "to-regex-range": "^5.0.1" 1299 | } 1300 | }, 1301 | "fs.realpath": { 1302 | "version": "1.0.0", 1303 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1304 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 1305 | "dev": true 1306 | }, 1307 | "fsevents": { 1308 | "version": "2.3.2", 1309 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 1310 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1311 | "dev": true, 1312 | "optional": true 1313 | }, 1314 | "function-bind": { 1315 | "version": "1.1.1", 1316 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 1317 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 1318 | "dev": true 1319 | }, 1320 | "get-port": { 1321 | "version": "3.2.0", 1322 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", 1323 | "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" 1324 | }, 1325 | "glob": { 1326 | "version": "7.1.7", 1327 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 1328 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 1329 | "dev": true, 1330 | "requires": { 1331 | "fs.realpath": "^1.0.0", 1332 | "inflight": "^1.0.4", 1333 | "inherits": "2", 1334 | "minimatch": "^3.0.4", 1335 | "once": "^1.3.0", 1336 | "path-is-absolute": "^1.0.0" 1337 | } 1338 | }, 1339 | "glob-parent": { 1340 | "version": "5.1.2", 1341 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1342 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1343 | "dev": true, 1344 | "requires": { 1345 | "is-glob": "^4.0.1" 1346 | } 1347 | }, 1348 | "has": { 1349 | "version": "1.0.3", 1350 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 1351 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 1352 | "dev": true, 1353 | "requires": { 1354 | "function-bind": "^1.1.1" 1355 | } 1356 | }, 1357 | "has-flag": { 1358 | "version": "3.0.0", 1359 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 1360 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 1361 | "dev": true 1362 | }, 1363 | "inflight": { 1364 | "version": "1.0.6", 1365 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1366 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 1367 | "dev": true, 1368 | "requires": { 1369 | "once": "^1.3.0", 1370 | "wrappy": "1" 1371 | } 1372 | }, 1373 | "inherits": { 1374 | "version": "2.0.4", 1375 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1376 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1377 | "dev": true 1378 | }, 1379 | "is-binary-path": { 1380 | "version": "2.1.0", 1381 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1382 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1383 | "dev": true, 1384 | "requires": { 1385 | "binary-extensions": "^2.0.0" 1386 | } 1387 | }, 1388 | "is-core-module": { 1389 | "version": "2.4.0", 1390 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", 1391 | "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", 1392 | "dev": true, 1393 | "requires": { 1394 | "has": "^1.0.3" 1395 | } 1396 | }, 1397 | "is-extglob": { 1398 | "version": "2.1.1", 1399 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1400 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 1401 | "dev": true 1402 | }, 1403 | "is-glob": { 1404 | "version": "4.0.1", 1405 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 1406 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 1407 | "dev": true, 1408 | "requires": { 1409 | "is-extglob": "^2.1.1" 1410 | } 1411 | }, 1412 | "is-module": { 1413 | "version": "1.0.0", 1414 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 1415 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 1416 | "dev": true 1417 | }, 1418 | "is-number": { 1419 | "version": "7.0.0", 1420 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1421 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1422 | "dev": true 1423 | }, 1424 | "is-reference": { 1425 | "version": "1.2.1", 1426 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", 1427 | "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", 1428 | "dev": true, 1429 | "requires": { 1430 | "@types/estree": "*" 1431 | } 1432 | }, 1433 | "jest-worker": { 1434 | "version": "26.6.2", 1435 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", 1436 | "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", 1437 | "dev": true, 1438 | "requires": { 1439 | "@types/node": "*", 1440 | "merge-stream": "^2.0.0", 1441 | "supports-color": "^7.0.0" 1442 | }, 1443 | "dependencies": { 1444 | "has-flag": { 1445 | "version": "4.0.0", 1446 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1447 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1448 | "dev": true 1449 | }, 1450 | "supports-color": { 1451 | "version": "7.2.0", 1452 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1453 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1454 | "dev": true, 1455 | "requires": { 1456 | "has-flag": "^4.0.0" 1457 | } 1458 | } 1459 | } 1460 | }, 1461 | "js-tokens": { 1462 | "version": "4.0.0", 1463 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1464 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 1465 | "dev": true 1466 | }, 1467 | "kleur": { 1468 | "version": "3.0.3", 1469 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 1470 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" 1471 | }, 1472 | "livereload": { 1473 | "version": "0.9.3", 1474 | "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", 1475 | "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", 1476 | "dev": true, 1477 | "requires": { 1478 | "chokidar": "^3.5.0", 1479 | "livereload-js": "^3.3.1", 1480 | "opts": ">= 1.2.0", 1481 | "ws": "^7.4.3" 1482 | } 1483 | }, 1484 | "livereload-js": { 1485 | "version": "3.3.2", 1486 | "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz", 1487 | "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", 1488 | "dev": true 1489 | }, 1490 | "local-access": { 1491 | "version": "1.1.0", 1492 | "resolved": "https://registry.npmjs.org/local-access/-/local-access-1.1.0.tgz", 1493 | "integrity": "sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==" 1494 | }, 1495 | "magic-string": { 1496 | "version": "0.25.7", 1497 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 1498 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 1499 | "dev": true, 1500 | "requires": { 1501 | "sourcemap-codec": "^1.4.4" 1502 | } 1503 | }, 1504 | "merge-stream": { 1505 | "version": "2.0.0", 1506 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 1507 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 1508 | "dev": true 1509 | }, 1510 | "mime": { 1511 | "version": "2.5.2", 1512 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", 1513 | "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" 1514 | }, 1515 | "minimatch": { 1516 | "version": "3.0.4", 1517 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1518 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1519 | "dev": true, 1520 | "requires": { 1521 | "brace-expansion": "^1.1.7" 1522 | } 1523 | }, 1524 | "mri": { 1525 | "version": "1.1.6", 1526 | "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz", 1527 | "integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==" 1528 | }, 1529 | "normalize-path": { 1530 | "version": "3.0.0", 1531 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1532 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1533 | "dev": true 1534 | }, 1535 | "once": { 1536 | "version": "1.4.0", 1537 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1538 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1539 | "dev": true, 1540 | "requires": { 1541 | "wrappy": "1" 1542 | } 1543 | }, 1544 | "opts": { 1545 | "version": "2.0.2", 1546 | "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", 1547 | "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", 1548 | "dev": true 1549 | }, 1550 | "path-is-absolute": { 1551 | "version": "1.0.1", 1552 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1553 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1554 | "dev": true 1555 | }, 1556 | "path-parse": { 1557 | "version": "1.0.7", 1558 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 1559 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1560 | "dev": true 1561 | }, 1562 | "picomatch": { 1563 | "version": "2.3.0", 1564 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 1565 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", 1566 | "dev": true 1567 | }, 1568 | "randombytes": { 1569 | "version": "2.1.0", 1570 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1571 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1572 | "dev": true, 1573 | "requires": { 1574 | "safe-buffer": "^5.1.0" 1575 | } 1576 | }, 1577 | "readdirp": { 1578 | "version": "3.6.0", 1579 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1580 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1581 | "dev": true, 1582 | "requires": { 1583 | "picomatch": "^2.2.1" 1584 | } 1585 | }, 1586 | "require-relative": { 1587 | "version": "0.8.7", 1588 | "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", 1589 | "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", 1590 | "dev": true 1591 | }, 1592 | "resolve": { 1593 | "version": "1.20.0", 1594 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 1595 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 1596 | "dev": true, 1597 | "requires": { 1598 | "is-core-module": "^2.2.0", 1599 | "path-parse": "^1.0.6" 1600 | } 1601 | }, 1602 | "rollup": { 1603 | "version": "2.52.8", 1604 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.8.tgz", 1605 | "integrity": "sha512-IjAB0C6KK5/lvqzJWAzsvOik+jV5Bt907QdkQ/gDP4j+R9KYNI1tjqdxiPitGPVrWC21Mf/ucXgowUjN/VemaQ==", 1606 | "dev": true, 1607 | "requires": { 1608 | "fsevents": "~2.3.2" 1609 | } 1610 | }, 1611 | "rollup-plugin-css-only": { 1612 | "version": "3.1.0", 1613 | "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz", 1614 | "integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==", 1615 | "dev": true, 1616 | "requires": { 1617 | "@rollup/pluginutils": "4" 1618 | }, 1619 | "dependencies": { 1620 | "@rollup/pluginutils": { 1621 | "version": "4.1.0", 1622 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz", 1623 | "integrity": "sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ==", 1624 | "dev": true, 1625 | "requires": { 1626 | "estree-walker": "^2.0.1", 1627 | "picomatch": "^2.2.2" 1628 | } 1629 | } 1630 | } 1631 | }, 1632 | "rollup-plugin-livereload": { 1633 | "version": "2.0.5", 1634 | "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz", 1635 | "integrity": "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==", 1636 | "dev": true, 1637 | "requires": { 1638 | "livereload": "^0.9.1" 1639 | } 1640 | }, 1641 | "rollup-plugin-svelte": { 1642 | "version": "7.1.0", 1643 | "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", 1644 | "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", 1645 | "dev": true, 1646 | "requires": { 1647 | "require-relative": "^0.8.7", 1648 | "rollup-pluginutils": "^2.8.2" 1649 | } 1650 | }, 1651 | "rollup-plugin-terser": { 1652 | "version": "7.0.2", 1653 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", 1654 | "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", 1655 | "dev": true, 1656 | "requires": { 1657 | "@babel/code-frame": "^7.10.4", 1658 | "jest-worker": "^26.2.1", 1659 | "serialize-javascript": "^4.0.0", 1660 | "terser": "^5.0.0" 1661 | } 1662 | }, 1663 | "rollup-pluginutils": { 1664 | "version": "2.8.2", 1665 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 1666 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 1667 | "dev": true, 1668 | "requires": { 1669 | "estree-walker": "^0.6.1" 1670 | }, 1671 | "dependencies": { 1672 | "estree-walker": { 1673 | "version": "0.6.1", 1674 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 1675 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 1676 | "dev": true 1677 | } 1678 | } 1679 | }, 1680 | "sade": { 1681 | "version": "1.7.4", 1682 | "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz", 1683 | "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==", 1684 | "requires": { 1685 | "mri": "^1.1.0" 1686 | } 1687 | }, 1688 | "safe-buffer": { 1689 | "version": "5.2.1", 1690 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1691 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1692 | "dev": true 1693 | }, 1694 | "semiver": { 1695 | "version": "1.1.0", 1696 | "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", 1697 | "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==" 1698 | }, 1699 | "serialize-javascript": { 1700 | "version": "4.0.0", 1701 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", 1702 | "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", 1703 | "dev": true, 1704 | "requires": { 1705 | "randombytes": "^2.1.0" 1706 | } 1707 | }, 1708 | "sirv": { 1709 | "version": "1.0.12", 1710 | "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.12.tgz", 1711 | "integrity": "sha512-+jQoCxndz7L2tqQL4ZyzfDhky0W/4ZJip3XoOuxyQWnAwMxindLl3Xv1qT4x1YX/re0leShvTm8Uk0kQspGhBg==", 1712 | "requires": { 1713 | "@polka/url": "^1.0.0-next.15", 1714 | "mime": "^2.3.1", 1715 | "totalist": "^1.0.0" 1716 | } 1717 | }, 1718 | "sirv-cli": { 1719 | "version": "1.0.12", 1720 | "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-1.0.12.tgz", 1721 | "integrity": "sha512-Rs5PvF3a48zuLmrl8vcqVv9xF/WWPES19QawVkpdzqx7vD5SMZS07+ece1gK4umbslXN43YeIksYtQM5csgIzQ==", 1722 | "requires": { 1723 | "console-clear": "^1.1.0", 1724 | "get-port": "^3.2.0", 1725 | "kleur": "^3.0.0", 1726 | "local-access": "^1.0.1", 1727 | "sade": "^1.6.0", 1728 | "semiver": "^1.0.0", 1729 | "sirv": "^1.0.12", 1730 | "tinydate": "^1.0.0" 1731 | } 1732 | }, 1733 | "source-map": { 1734 | "version": "0.7.3", 1735 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 1736 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 1737 | "dev": true 1738 | }, 1739 | "source-map-support": { 1740 | "version": "0.5.19", 1741 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 1742 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 1743 | "dev": true, 1744 | "requires": { 1745 | "buffer-from": "^1.0.0", 1746 | "source-map": "^0.6.0" 1747 | }, 1748 | "dependencies": { 1749 | "source-map": { 1750 | "version": "0.6.1", 1751 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1752 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1753 | "dev": true 1754 | } 1755 | } 1756 | }, 1757 | "sourcemap-codec": { 1758 | "version": "1.4.8", 1759 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 1760 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 1761 | "dev": true 1762 | }, 1763 | "supports-color": { 1764 | "version": "5.5.0", 1765 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1766 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1767 | "dev": true, 1768 | "requires": { 1769 | "has-flag": "^3.0.0" 1770 | } 1771 | }, 1772 | "svelte": { 1773 | "version": "3.38.3", 1774 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.38.3.tgz", 1775 | "integrity": "sha512-N7bBZJH0iF24wsalFZF+fVYMUOigaAUQMIcEKHO3jstK/iL8VmP9xE+P0/a76+FkNcWt+TDv2Gx1taUoUscrvw==", 1776 | "dev": true 1777 | }, 1778 | "terser": { 1779 | "version": "5.7.1", 1780 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz", 1781 | "integrity": "sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==", 1782 | "dev": true, 1783 | "requires": { 1784 | "commander": "^2.20.0", 1785 | "source-map": "~0.7.2", 1786 | "source-map-support": "~0.5.19" 1787 | } 1788 | }, 1789 | "tinydate": { 1790 | "version": "1.3.0", 1791 | "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", 1792 | "integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==" 1793 | }, 1794 | "to-regex-range": { 1795 | "version": "5.0.1", 1796 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1797 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1798 | "dev": true, 1799 | "requires": { 1800 | "is-number": "^7.0.0" 1801 | } 1802 | }, 1803 | "totalist": { 1804 | "version": "1.1.0", 1805 | "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", 1806 | "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==" 1807 | }, 1808 | "wrappy": { 1809 | "version": "1.0.2", 1810 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1811 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1812 | "dev": true 1813 | }, 1814 | "ws": { 1815 | "version": "7.5.2", 1816 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.2.tgz", 1817 | "integrity": "sha512-lkF7AWRicoB9mAgjeKbGqVUekLnSNO4VjKVnuPHpQeOxZOErX6BPXwJk70nFslRCEEA8EVW7ZjKwXaP9N+1sKQ==", 1818 | "dev": true, 1819 | "requires": {} 1820 | } 1821 | } 1822 | } 1823 | -------------------------------------------------------------------------------- /docs/docs-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CDN Docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.0", 13 | "rollup": "^2.3.4", 14 | "rollup-plugin-css-only": "^3.1.0", 15 | "rollup-plugin-livereload": "^2.0.0", 16 | "rollup-plugin-svelte": "^7.0.0", 17 | "rollup-plugin-terser": "^7.0.0", 18 | "svelte": "^3.0.0" 19 | }, 20 | "dependencies": { 21 | "sirv-cli": "^1.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/docs-template/public/assets/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tech-With-Tim/cdn/741e7483e6858ed5f3f2169a63adcc7f8a9ba2a2/docs/docs-template/public/assets/arrow.png -------------------------------------------------------------------------------- /docs/docs-template/public/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tech-With-Tim/cdn/741e7483e6858ed5f3f2169a63adcc7f8a9ba2a2/docs/docs-template/public/assets/favicon.png -------------------------------------------------------------------------------- /docs/docs-template/public/assets/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /docs/docs-template/public/assets/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tech-With-Tim/cdn/741e7483e6858ed5f3f2169a63adcc7f8a9ba2a2/docs/docs-template/public/assets/link.png -------------------------------------------------------------------------------- /docs/docs-template/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tech-With-Tim/cdn/741e7483e6858ed5f3f2169a63adcc7f8a9ba2a2/docs/docs-template/public/favicon.png -------------------------------------------------------------------------------- /docs/docs-template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tech With Tim CDN 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 47 | 48 | -------------------------------------------------------------------------------- /docs/docs-template/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import css from 'rollup-plugin-css-only'; 7 | 8 | const production = !process.env.ROLLUP_WATCH; 9 | 10 | function serve() { 11 | let server; 12 | 13 | function toExit() { 14 | if (server) server.kill(0); 15 | } 16 | 17 | return { 18 | writeBundle() { 19 | if (server) return; 20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 21 | stdio: ['ignore', 'inherit', 'inherit'], 22 | shell: true 23 | }); 24 | 25 | process.on('SIGTERM', toExit); 26 | process.on('exit', toExit); 27 | } 28 | }; 29 | } 30 | 31 | export default { 32 | input: 'src/main.js', 33 | output: { 34 | sourcemap: true, 35 | format: 'iife', 36 | name: 'app', 37 | file: 'public/build/bundle.js' 38 | }, 39 | plugins: [ 40 | svelte({ 41 | compilerOptions: { 42 | // enable run-time checks when not in production 43 | dev: !production 44 | } 45 | }), 46 | // we'll extract any component CSS out into 47 | // a separate file - better for performance 48 | css({ output: 'bundle.css' }), 49 | 50 | // If you have external dependencies installed from 51 | // npm, you'll most likely need these plugins. In 52 | // some cases you'll need additional configuration - 53 | // consult the documentation for details: 54 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 55 | resolve({ 56 | browser: true, 57 | dedupe: ['svelte'] 58 | }), 59 | commonjs(), 60 | 61 | // In dev mode, call `npm run start` once 62 | // the bundle has been generated 63 | !production && serve(), 64 | 65 | // Watch the `public` directory and refresh the 66 | // browser on changes when not in production 67 | !production && livereload('public'), 68 | 69 | // If we're building for production (npm run build 70 | // instead of npm run dev), minify 71 | production && terser() 72 | ], 73 | watch: { 74 | clearScreen: false 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /docs/docs-template/scripts/setupTypeScript.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** This script modifies the project to support TS code in .svelte files like: 4 | 5 | 8 | 9 | As well as validating the code for CI. 10 | */ 11 | 12 | /** To work on this script: 13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template 14 | */ 15 | 16 | const fs = require("fs") 17 | const path = require("path") 18 | const { argv } = require("process") 19 | 20 | const projectRoot = argv[2] || path.join(__dirname, "..") 21 | 22 | // Add deps to pkg.json 23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) 24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { 25 | "svelte-check": "^2.0.0", 26 | "svelte-preprocess": "^4.0.0", 27 | "@rollup/plugin-typescript": "^8.0.0", 28 | "typescript": "^4.0.0", 29 | "tslib": "^2.0.0", 30 | "@tsconfig/svelte": "^2.0.0" 31 | }) 32 | 33 | // Add script for checking 34 | packageJSON.scripts = Object.assign(packageJSON.scripts, { 35 | "check": "svelte-check --tsconfig ./tsconfig.json" 36 | }) 37 | 38 | // Write the package JSON 39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) 40 | 41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too 42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js") 43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts") 44 | fs.renameSync(beforeMainJSPath, afterMainTSPath) 45 | 46 | // Switch the app.svelte file to use TS 47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte") 48 | let appFile = fs.readFileSync(appSveltePath, "utf8") 49 | appFile = appFile.replace(" 16 | 17 |
18 | 19 | 20 | 23 | 24 | 27 | 28 |
25 | 26 |
29 |
30 | 31 | 50 | -------------------------------------------------------------------------------- /docs/docs-template/src/Docs.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 |
12 |
13 | Tech With Tim CDN Documentation 14 |
15 |
18 | 19 |
20 | {#each routes as route} 21 |
22 | 23 | 24 | 37 | 38 | 46 | 47 |
25 | 29 | link 35 | 36 | 39 |
43 | { route.Name } { route.Route } 44 |
45 |
48 | 49 |
50 |
53 | {route.Description} 54 |
55 | 56 | 57 | URL Parameters: 58 | 59 | 60 | {route.URLParameters} 61 | 62 | 63 |
64 | 65 | 66 | Response: 67 | 68 | 69 | {route.Response} 70 | 71 | 72 |
73 | 74 | 75 | Request Body: 76 | 77 | 78 | {#if !route.RequestBody} 79 | None 80 | {/if} 81 | 82 |
83 | 84 | {#if route.RequestBody} 85 | 86 | 87 | 90 | 93 | 96 | 97 | {#each route.RequestBody as reqbody} 98 | 99 | 100 | 101 | 102 | 103 | {/each} 104 |
88 | Field 89 | 91 | Type 92 | 94 | Description 95 |
{reqbody.Name}{reqbody.Type}{reqbody.Desc}
105 | {/if} 106 | 107 |
108 |
109 | {/each} 110 |
111 | 112 |
113 | 114 | -------------------------------------------------------------------------------- /docs/docs-template/src/RouteList.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 |
32 | 33 | {#if routeList.length != 0} 34 | {#each routeList as route} 35 | 36 | 39 | 40 | 45 | 50 | 51 | 52 |
41 | 42 | {route.Name} 43 | 44 | 46 | 47 | 48 | 49 |
53 | {/each} 54 | {:else} 55 |
No results found
56 | {/if} 57 |
58 |
59 | 60 | -------------------------------------------------------------------------------- /docs/docs-template/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | intro: true 6 | }); 7 | 8 | export default app; -------------------------------------------------------------------------------- /docs/register.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os/exec" 8 | "strings" 9 | 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | var routes []RouteInfo = []RouteInfo{} 14 | 15 | type Docs struct { 16 | Content RouteInfo `yaml:"Content"` 17 | } 18 | 19 | type RouteInfo struct { 20 | Response string `yaml:"Response"` 21 | URLParameters string `yaml:"URL Parameters"` 22 | 23 | Name string 24 | Route string 25 | 26 | RequestBody []struct { 27 | Name string `yaml:"Name"` 28 | Type string `yaml:"Type"` 29 | Desc string `yaml:"Description"` 30 | } `yaml:"Request Body"` 31 | 32 | Description string `yaml:"Description"` 33 | } 34 | 35 | // Extract comments to form docs 36 | func AddDocs(route, funcName string) error { 37 | 38 | rawFunc := funcName 39 | 40 | funcName = strings.ReplaceAll(funcName, " ", "") 41 | output, err := exec.Command("go", "doc", funcName).Output() 42 | 43 | if err != nil { 44 | return err 45 | } 46 | 47 | rawDocs := "Content:\n" 48 | 49 | for num, line := range strings.Split(string(output), "\n") { 50 | if num >= 3 { 51 | rawDocs += line + "\n" 52 | } 53 | } 54 | 55 | source := []byte(rawDocs) 56 | 57 | var config Docs 58 | err = yaml.Unmarshal(source, &config) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | config.Content.Name = rawFunc 64 | config.Content.Route = route 65 | 66 | routes = append(routes, config.Content) 67 | 68 | return nil 69 | } 70 | 71 | // Write docs to a json file 72 | func GenerateDocs() { 73 | 74 | jsonData, _ := json.MarshalIndent(routes, "", " ") 75 | 76 | err := ioutil.WriteFile("../../docs/docs-template/public/docs.json", []byte(jsonData), 0644) 77 | 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Tech-With-Tim/cdn 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/caarlos0/env v3.5.0+incompatible 7 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 8 | github.com/go-chi/chi/v5 v5.0.3 9 | github.com/go-chi/cors v1.2.0 10 | github.com/go-redis/redis/v7 v7.4.0 11 | github.com/golang-jwt/jwt v3.2.1+incompatible 12 | github.com/golang-migrate/migrate/v4 v4.14.1 13 | github.com/lib/pq v1.10.1 14 | github.com/omeid/pgerror v0.0.0-20201018020948-42c66c4d27d4 15 | github.com/spf13/viper v1.8.0 16 | github.com/stretchr/testify v1.7.0 17 | github.com/urfave/cli/v2 v2.3.0 18 | golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect 19 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 20 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect 21 | gopkg.in/yaml.v2 v2.4.0 22 | ) 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "path/filepath" 7 | 8 | "github.com/Tech-With-Tim/cdn/api" 9 | "github.com/Tech-With-Tim/cdn/api/handlers" 10 | "github.com/Tech-With-Tim/cdn/docs" 11 | "github.com/Tech-With-Tim/cdn/server" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-chi/chi/v5/middleware" 14 | 15 | // _ "net/http/pprof" // only in use when profiling 16 | "os" 17 | 18 | "github.com/Tech-With-Tim/cdn/utils" 19 | "github.com/urfave/cli/v2" 20 | ) 21 | 22 | var app = cli.NewApp() 23 | 24 | func main() { 25 | //Export Env Variables If exist 26 | //err := utils.ExportVariables() 27 | config, err := utils.LoadConfig("./", "app") 28 | if err != nil { 29 | log.Fatalln(err.Error()) 30 | } 31 | //Register Commands 32 | commands(config) 33 | 34 | err = app.Run(os.Args) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | } 39 | 40 | func loadconfig(testconf bool) (conf utils.Config, err error) { 41 | if testconf { 42 | conf, err = utils.LoadConfig("./", "test") 43 | } else { 44 | conf, err = utils.LoadConfig("./", "app") 45 | } 46 | return 47 | } 48 | 49 | func commands(config utils.Config) { 50 | app.Commands = []*cli.Command{ 51 | { 52 | Name: "migrate_up", 53 | Usage: "Migrate DB to latest version", 54 | Flags: []cli.Flag{ 55 | &cli.BoolFlag{ 56 | Name: "test", 57 | Aliases: []string{"t"}, 58 | Usage: "loads test.env instead of app.env", 59 | }, 60 | }, 61 | Action: func(c *cli.Context) error { 62 | conf, err := loadconfig(c.Bool("test")) 63 | if err != nil { 64 | return err 65 | } 66 | err = utils.MigrateUp(conf, "./models/migrations/") 67 | if err != nil { 68 | return err 69 | } 70 | return nil 71 | }, 72 | }, 73 | { 74 | Name: "dropdb", 75 | Usage: "Drop the DB", 76 | Flags: []cli.Flag{ 77 | &cli.BoolFlag{ 78 | Name: "test", 79 | Aliases: []string{"t"}, 80 | Usage: "loads test.env instead of app.env", 81 | }, 82 | }, 83 | Action: func(c *cli.Context) error { 84 | conf, err := loadconfig(c.Bool("test")) 85 | if err != nil { 86 | return err 87 | } 88 | err = utils.MigrateDown(conf, "./models/migrations/") 89 | if err != nil { 90 | return err 91 | } 92 | return nil 93 | }, 94 | }, 95 | { 96 | Name: "migrate_steps", 97 | Usage: "Migrate with Steps", 98 | Flags: []cli.Flag{ 99 | &cli.IntFlag{ 100 | Name: "steps", 101 | Usage: "Number of steps of migrations to run", 102 | }, 103 | &cli.BoolFlag{ 104 | Name: "test", 105 | Aliases: []string{"t"}, 106 | Usage: "loads test.env instead of app.env", 107 | }, 108 | }, 109 | Action: func(c *cli.Context) error { 110 | conf, err := loadconfig(c.Bool("test")) 111 | if err != nil { 112 | return err 113 | } 114 | err = utils.MigrateSteps(c.Int("steps"), conf, "./models/migrations/") 115 | if err != nil { 116 | return err 117 | } 118 | return nil 119 | }, 120 | }, 121 | { 122 | Name: "generate_docs", 123 | Usage: "Generate Documentation for the CDN", 124 | Action: func(_ *cli.Context) error { 125 | 126 | err := os.Chdir("./api/handlers") 127 | 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | for route, handler := range api.Routes { 133 | err := docs.AddDocs(route, handler) 134 | 135 | if err != nil { 136 | log.Fatal(err) 137 | } 138 | } 139 | 140 | docs.GenerateDocs() 141 | 142 | return nil 143 | }, 144 | }, 145 | { 146 | Name: "runserver", 147 | Usage: "Run Api Server", 148 | Flags: []cli.Flag{ 149 | &cli.StringFlag{ 150 | Name: "host", 151 | Usage: "Host on which server has to be run", 152 | Value: "localhost", 153 | Aliases: []string{"H"}, 154 | }, 155 | &cli.IntFlag{ 156 | Name: "port", 157 | Usage: "Port on which server has to be run", 158 | Value: 5000, 159 | Aliases: []string{"P"}, 160 | }, 161 | }, 162 | Action: func(c *cli.Context) error { 163 | s := server.NewServer(config) 164 | //Create Routers Here 165 | CdnRouter := chi.NewRouter() 166 | 167 | //Add Routes to Routers Here 168 | services := handlers.NewServiceHandler(s.Store, *s.Cache) 169 | api.MainRouter(CdnRouter, config, services) 170 | 171 | workDir, _ := os.Getwd() 172 | filesDir := http.Dir(filepath.Join(workDir, "docs/docs-template/public")) 173 | server.FileServer(s.Router, "/docs", filesDir) 174 | 175 | //Mount Routers here 176 | s.Router.Mount("/", CdnRouter) 177 | s.Router.Mount("/debug/", middleware.Profiler()) // Only in use when profiling 178 | //Store Router in Struct 179 | err := s.RunServer(c.String("host"), c.Int("port")) 180 | return err 181 | }, 182 | }, 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | //"fmt" 5 | //"log" 6 | //"net/http" 7 | //"time" 8 | 9 | "database/sql" 10 | "fmt" 11 | "log" 12 | "net/http" 13 | "strings" 14 | 15 | "time" 16 | 17 | "github.com/Tech-With-Tim/cdn/cache" 18 | db "github.com/Tech-With-Tim/cdn/db/sqlc" 19 | "github.com/Tech-With-Tim/cdn/utils" 20 | 21 | "github.com/go-chi/chi/v5" 22 | "github.com/go-chi/chi/v5/middleware" 23 | "github.com/go-chi/cors" 24 | _ "github.com/lib/pq" 25 | ) 26 | 27 | type Server struct { 28 | Router *chi.Mux 29 | Store *db.Store 30 | Cache *cache.PostCache 31 | } 32 | 33 | func NewServer(config utils.Config) *Server { 34 | s := &Server{} 35 | err := s.PrepareDB(config) 36 | if err != nil { 37 | log.Fatalln(err.Error()) 38 | } 39 | s.PrepareRedis(config) 40 | s.PrepareRouter() 41 | return s 42 | } 43 | 44 | func (s *Server) PrepareRedis(config utils.Config) { 45 | cache := cache.NewRedisCache( 46 | config.RedisHost, 47 | config.RedisDb, 48 | config.RedisPass, 49 | time.Hour*24) 50 | s.Cache = &cache 51 | } 52 | 53 | func (s *Server) PrepareDB(config utils.Config) (err error) { 54 | 55 | //Connect to db, else exit 0 56 | dbSource := config.DBUri 57 | DB, err := sql.Open("postgres", dbSource) 58 | if err != nil { 59 | return 60 | } 61 | s.Store = db.NewStore(DB) 62 | log.Println("Connected to the Database") 63 | return 64 | } 65 | 66 | func (s *Server) PrepareRouter() { 67 | r := chi.NewRouter() 68 | 69 | //Use Global Middlewares Here 70 | r.Use(middleware.RequestID) 71 | r.Use(middleware.RealIP) 72 | r.Use(middleware.Logger) 73 | r.Use(middleware.Recoverer) 74 | r.Use(middleware.Timeout(60 * time.Second)) 75 | r.Use(cors.Handler(cors.Options{ 76 | // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts 77 | AllowedOrigins: []string{"https://*", "http://*"}, 78 | // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, 79 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, 80 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, 81 | ExposedHeaders: []string{"Link"}, 82 | AllowCredentials: false, 83 | MaxAge: 300, // Maximum value not ignored by any of major browsers 84 | })) 85 | //Store Router in Struct 86 | s.Router = r 87 | } 88 | 89 | func (s *Server) RunServer(host string, port int) (err error) { 90 | log.Printf("Starting Server at %s:%v", host, port) 91 | err = http.ListenAndServe(fmt.Sprintf("%s:%v", host, port), s.Router) 92 | return 93 | } 94 | 95 | // FileServer conveniently sets up a http.FileServer handler to serve 96 | // static files from a http.FileSystem. 97 | func FileServer(r chi.Router, path string, root http.FileSystem) { 98 | if strings.ContainsAny(path, "{}*") { 99 | panic("FileServer does not permit any URL parameters.") 100 | } 101 | 102 | if path != "/" && path[len(path)-1] != '/' { 103 | r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) 104 | path += "/" 105 | } 106 | path += "*" 107 | 108 | r.Get(path, func(w http.ResponseWriter, r *http.Request) { 109 | rctx := chi.RouteContext(r.Context()) 110 | pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") 111 | fs := http.StripPrefix(pathPrefix, http.FileServer(root)) 112 | fs.ServeHTTP(w, r) 113 | }) 114 | 115 | } 116 | -------------------------------------------------------------------------------- /sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "1" 2 | packages: 3 | - name: "db" 4 | path: "./db/sqlc" 5 | queries: "./db/query/" 6 | schema: "./models/migrations/" 7 | engine: "postgresql" 8 | emit_prepared_queries: false 9 | emit_interface: false 10 | emit_exact_table_names: true 11 | emit_empty_slices: false 12 | emit_json_tags: true 13 | json_tags_case_style: "camel" -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | go test ./... -p 1 -v -coverprofile cover.out | sed ''/PASS/s//$(printf "\033[32mPASS\033[0m")/'' | sed ''/FAIL/s//$(printf "\033[31mFAIL\033[0m")/'' | sed ''/RUN/s//$(printf "\033[33mRUN\033[0m")/'' | GREP_COLOR='01;32' grep -E --color 'ok.*$|^.*ok.*$|$' -------------------------------------------------------------------------------- /utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/caarlos0/env" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // Config stores all configuration of the application 9 | type Config struct { 10 | DBUri string `mapstructure:"DB_URI" env:"DB_URI"` 11 | SecretKey string `mapstructure:"SECRET_KEY" env:"SECRET_KEY"` 12 | RedisPass string `mapstructure:"REDIS_PASS" env:"REDIS_PASS"` 13 | RedisHost string `mapstructure:"REDIS_HOST" env:"REDIS_HOST"` 14 | RedisDb int `mapstructure:"REDIS_DB" env:"REDIS_DB"` 15 | MaxFileSize int64 `mapstructure:"MAX_FILE_SIZE" env:"MAX_FILE_SIZE"` 16 | } 17 | 18 | func LoadConfig(path string, name string) (config Config, err error) { 19 | viper.AddConfigPath(path) 20 | viper.SetConfigName(name) 21 | viper.SetConfigType("env") 22 | viper.AutomaticEnv() 23 | 24 | err = viper.ReadInConfig() 25 | if err != nil { 26 | err = env.Parse(&config) 27 | if err != nil { 28 | return 29 | } 30 | if config.SecretKey != "" { 31 | if config.MaxFileSize != 0 { 32 | err = nil 33 | return 34 | } 35 | } 36 | return 37 | } 38 | err = viper.Unmarshal(&config) 39 | 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /utils/main_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | var config Config 10 | 11 | func TestMain(m *testing.M) { 12 | var err error 13 | config, err = LoadConfig("../", "test") 14 | if err != nil { 15 | log.Fatalln(err) 16 | } 17 | os.Exit(m.Run()) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /utils/migration.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/golang-migrate/migrate/v4" 8 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 9 | _ "github.com/golang-migrate/migrate/v4/source/file" 10 | _ "github.com/golang-migrate/migrate/v4/source/github" 11 | ) 12 | 13 | func MigrateUp(config Config, path string) error { 14 | m, err := migrate.New("file://"+path, config.DBUri) 15 | if err != nil { 16 | return err 17 | } 18 | err = m.Up() 19 | if err != nil { 20 | return err 21 | } 22 | log.Println("Migrated To Latest Version") 23 | return nil 24 | } 25 | 26 | func MigrateDown(config Config, path string) error { 27 | m, err := migrate.New("file://"+path, config.DBUri) 28 | 29 | if err != nil { 30 | return err 31 | } 32 | err = m.Down() 33 | if err != nil { 34 | return err 35 | } 36 | fmt.Println("Migrated to lowest Version...") 37 | return nil 38 | } 39 | 40 | func MigrateSteps(steps int, config Config, path string) error { 41 | m, err := migrate.New("file://"+path, config.DBUri) 42 | if err != nil { 43 | return err 44 | } 45 | err = m.Steps(steps) 46 | if err != nil { 47 | return err 48 | } 49 | fmt.Printf("Migrated %v steps \n", steps) 50 | return nil 51 | } 52 | 53 | //func GetDbCredentials(config Config) string { 54 | // return fmt.Sprintf("user=%s sslmode=disable password=%s host=%s port=%v dbname=%s", 55 | // config.PostgresUser, 56 | // config.PostgresPassword, 57 | // config.DbHost, 58 | // config.DbPort, 59 | // config.DbName) 60 | //} 61 | -------------------------------------------------------------------------------- /utils/migration_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang-migrate/migrate/v4" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMigrateUp(t *testing.T) { 11 | err := MigrateUp(config, "../models/migrations/") 12 | if err != nil { 13 | if err != migrate.ErrNoChange { 14 | require.NoError(t, err) 15 | } 16 | } 17 | } 18 | 19 | func TestMigrateDown(t *testing.T) { 20 | err := MigrateDown(config, "../models/migrations/") 21 | require.NoError(t, err) 22 | } 23 | 24 | func TestMigrateSteps(t *testing.T) { 25 | err := MigrateSteps(1, config, "../models/migrations/") 26 | require.NoError(t, err) 27 | } 28 | -------------------------------------------------------------------------------- /utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const charset = "abcdefghijklmnopqrstuvwxyz" + 11 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 12 | 13 | func init() { 14 | rand.Seed(time.Now().UnixNano()) 15 | } 16 | 17 | //RandomInt Generates a random number between min and max 18 | func RandomInt(min, max int64) int64 { 19 | return min + rand.Int63n(max-min+1) // +1 if max - min = 0 20 | } 21 | 22 | //RandomString Generates a Random String of N characters 23 | func RandomString(n int) string { 24 | var sb strings.Builder 25 | k := len(charset) 26 | 27 | for i := 0; i < n; i++ { 28 | c := charset[rand.Intn(k)] 29 | sb.WriteByte(c) 30 | } 31 | return sb.String() 32 | } 33 | 34 | func StrToBinary(s string, base int) []byte { 35 | 36 | var b []byte 37 | 38 | for _, c := range s { 39 | b = strconv.AppendInt(b, int64(c), base) 40 | } 41 | 42 | return b 43 | } 44 | -------------------------------------------------------------------------------- /utils/random_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRandomInt(t *testing.T) { 12 | n := 5 13 | var wg sync.WaitGroup 14 | values := make(chan int64) 15 | for i := 0; i < n; i++ { 16 | wg.Add(1) 17 | go func(wg *sync.WaitGroup) { 18 | defer wg.Done() 19 | values <- RandomInt(0, rand.Int63n(10000000)) 20 | }(&wg) 21 | } 22 | go func() { 23 | wg.Wait() 24 | close(values) 25 | }() 26 | for v := range values { 27 | require.NotZero(t, v) 28 | } 29 | } 30 | 31 | func TestRandomString(t *testing.T) { 32 | n := 5 33 | values := make(chan string) 34 | var wg sync.WaitGroup 35 | for i := 0; i < n; i++ { 36 | wg.Add(1) 37 | go func(wg *sync.WaitGroup) { 38 | defer wg.Done() 39 | values <- RandomString(50) 40 | }(&wg) 41 | } 42 | go func() { 43 | wg.Wait() 44 | close(values) 45 | }() 46 | for v := range values { 47 | require.NotEmpty(t, v) 48 | require.Len(t, v, 50) 49 | } 50 | } 51 | 52 | func TestStrToBinary(t *testing.T) { 53 | n := 5 54 | var wg sync.WaitGroup 55 | values := make(chan []byte) 56 | for i := 0; i < n; i++ { 57 | wg.Add(1) 58 | go func(wg *sync.WaitGroup) { 59 | defer wg.Done() 60 | values <- StrToBinary(RandomString(20), 10) 61 | }(&wg) 62 | } 63 | go func() { 64 | wg.Wait() 65 | close(values) 66 | }() 67 | for v := range values { 68 | require.NotEmpty(t, v) 69 | require.IsType(t, []byte{}, v) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /utils/responses.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // JSON returns a well formated response with a status code 9 | func JSON(w http.ResponseWriter, statusCode int, data interface{}) { 10 | w.WriteHeader(statusCode) 11 | err := json.NewEncoder(w).Encode(data) 12 | if err != nil { 13 | w.WriteHeader(500) 14 | er := json.NewEncoder(w).Encode(map[string]interface{}{"error": "something unexpected occurred."}) 15 | if er != nil { 16 | return 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------