├── .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 |
2 |
3 |
Tech With Tim - CDN
4 |
5 |
6 |
7 | 
8 | [](https://codecov.io/gh/Tech-With-Tim/cdn)
9 | [](https://github.com/Tech-With-Tim/cdn/actions/workflows/lint-test.yml)
10 | [](https://github.com/Tech-With-Tim/CDN/issues)
11 | [](https://github.com/Tech-With-Tim/CDN/pulls)
12 | [](/LICENCE)
13 | [](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 |
21 |
22 | |
23 |
24 |
25 |
26 | |
27 |
28 |
29 |
30 |
31 |
50 |
--------------------------------------------------------------------------------
/docs/docs-template/src/Docs.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |  |
10 |
11 |
12 |
13 | Tech With Tim CDN Documentation
14 |
15 | |
16 |
17 |
18 |
19 |
20 | {#each routes as route}
21 |
22 |
23 |
24 |
25 |
29 |
35 |
36 | |
37 |
38 |
39 |
43 | { route.Name } { route.Route }
44 |
45 | |
46 |
47 |
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 |
88 | Field
89 | |
90 |
91 | Type
92 | |
93 |
94 | Description
95 | |
96 |
97 | {#each route.RequestBody as reqbody}
98 |
99 | {reqbody.Name} |
100 | {reqbody.Type} |
101 | {reqbody.Desc} |
102 |
103 | {/each}
104 |
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 |
41 |
42 | {route.Name}
43 |
44 | |
45 | |
46 |
47 |
48 |
49 | |
50 |
51 |
52 |
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 |
--------------------------------------------------------------------------------