diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f5c0804 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# flyctl launch added from .gitignore +# Directories +**\.idea +**\.vscode +**\.history +**\.wrangler + +# Files +**\.env +**\.dev.vars + +# Binaries for programs and plugins +**\*.exe +**\*.exe~ +**\*.dll +**\*.so +**\*.dylib +# Test binary, built with `go test -c` +**\*.test +# Output of the go coverage tool, specifically when used with LiteIDE +**\*.out +**\*.db +**\*.zip +**\*.DS_Store + +# Dependency directories (remove the comment below to include it) +# vendor/ +# Go workspace file +**\go.work +**\go.work.sum + +fly.toml + +.git/ +.github/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 8ee8264..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "env": { - "browser": true, - "commonjs": false, - "es6": true, - "node": false - }, - "extends": [ - "eslint:recommended" - ], - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": [], - "root": true, - "rules": { - "array-bracket-newline": [ - "error", - "consistent" - ], - "array-bracket-spacing": [ - "error", - "never" - ], - "array-element-newline": [ - "error", - "consistent" - ], - "arrow-body-style": [ - "error", - "as-needed" - ], - "arrow-parens": [ - "error", - "always" - ], - "block-spacing": [ - "error", - "always" - ], - "brace-style": [ - "error", - "1tbs" - ], - "comma-dangle": [ - "error", - { - "arrays": "always-multiline", - "exports": "always-multiline", - "functions": "always-multiline", - "imports": "always-multiline", - "objects": "always-multiline" - } - ], - "comma-spacing": [ - "error", - { - "after": true, - "before": false - } - ], - "comma-style": [ - "error", - "last" - ], - "computed-property-spacing": [ - "error", - "never" - ], - "curly": [ - "error", - "multi-line", // "multi" - "consistent" - ], - "default-case-last": [ - "error" - ], - "dot-location": [ - "error", - "property" - ], - "dot-notation": [ - "error" - ], - "eqeqeq": [ - "error" - ], - "func-call-spacing": [ - "error", - "never" - ], - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "off", - "windows" - ], - "max-depth": [ - "warn", - { - "max": 5 - } - ], - "max-len": [ - "warn", - { - "code": 180, - "ignoreRegExpLiterals": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreTrailingComments": true, - "ignoreUrls": true - } - ], - "max-lines": [ - "warn" - ], - "max-statements-per-line": [ - "error" - ], - "multiline-comment-style": [ - "off" - ], - "no-console": [ - "off" - ], - "no-prototype-builtins": [ - "off" - ], - "no-return-assign": [ - "error" - ], - "no-template-curly-in-string": [ - "warn" - ], - "no-trailing-spaces": [ - "error" - ], - "no-underscore-dangle": [ - "warn", - { - "allowAfterThis": true, - "allowFunctionParams": true - } - ], - "no-unneeded-ternary": [ - "error" - ], - "no-unused-expressions": [ - "error" - ], - "no-var": [ - "error" - ], - "no-whitespace-before-property": [ - "error" - ], - "object-curly-newline": [ - "error" - // { - // "minProperties": 3, - // "multiline": true - // } - ], - "object-curly-spacing": [ - "error", - "always" - ], - "object-property-newline": [ - "error" - ], - "operator-linebreak": [ - "error" - ], - "prefer-arrow-callback": [ - "error" - ], - "prefer-const": [ - "error", - { - "destructuring": "all", - "ignoreReadBeforeAssign": false - } - ], - "quotes": [ - "error", - "single" - ], - "rest-spread-spacing": [ - "error", - "never" - ], - "semi": [ - "error", - "always" - ], - "sort-keys": [ - "warn", - "asc", - { - "natural": true - } - ], - "space-in-parens": [ - "error", - "never" - ], - "spaced-comment": [ - "error", - "always" - ] - } -} \ No newline at end of file diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..b0c246e --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 5f7abc4..3b77ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,28 @@ .vscode/ .history/ .wrangler/ -node_modules/ # Files .env -.dev.vars \ No newline at end of file +.dev.vars + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +# Test binary, built with `go test -c` +*.test +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +*.db +*.zip +*.DS_Store + +# Dependency directories (remove the comment below to include it) +# vendor/ +# Go workspace file +go.work +go.work.sum + diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index a14248e..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 100, - "trailingComma": "all", - "tabWidth": 2, - "useTabs": true, - "semi": true, - "singleQuote": true -} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea1c901 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.23 AS builder +WORKDIR /build +RUN apt-get update && apt-get install -y gcc +COPY go.mod go.sum ./ +RUN go mod download +COPY ./ ./ +RUN CGO_ENABLED=1 GOOS=linux go build -ldflags '-extldflags "-static"' -o ./api + +FROM golang:1.23 AS runner +COPY --from=builder /build/api /api +ENV SERVER_PORT=80 +EXPOSE 80 +CMD ["/api"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 9349203..e832a9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2019-2022 The Mue Authors +Copyright (c) 2024 Kaiso One Ltd +Copyright (c) 2019-2024 The Mue Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 87b8105..b2cd1f7 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,45 @@ -# Mue API -Source code for the Mue API. This API is the default for random images and quotes in Mue. +![mue-api-logo-banner](https://github.com/user-attachments/assets/e3747494-4034-41a2-b632-c4a7083b4585) + +## Table Of Contents + + +1. What is this API? +2. Installation +3. Env Variables +4. License + +## What is this API? + +The Mue API is created to serve HTTP requests for photos, quotes, weather and marketplace data to users. + +This API is used by the **[Mue Tab](https://github.com/mue/mue)** browser extension. ## Installation -See the [documentation](https://docs.muetab.com/development#api). - -## Environment - -Non-secret variables are in `wrangler.toml`. -Secrets are deployed with Wrangler but a `.dev.vars` file can be used in development: - -```sh -IS_LOCAL_MODE=1 -BASELIME_API_KEY= -MAPBOX_TOKEN= -OPENWEATHER_TOKEN= -PEXELS_TOKEN= -SUPABASE_TOKEN= -SUPABASE_URL= -UNSPLASH_TOKEN= + +To install this project, you will need the following on your machine: + +![Go](https://img.shields.io/badge/Go-00ADD8?logo=Go&logoColor=white&style=for-the-badge) + +Then run the commands: + +```bash +go get + +go run . ``` +## Env Variables + +| Name | Description | Required | Default value | Limitations | +| :-------------------: | :-------------------------------------------: | :------: | :-----------: | :----------------------------------------------: | +| `DATABASE_CACHE_FILE` | Local Cache Database Replica | ✅ | `replica.db` | Must be a string, must end in .db | +| `TURSO_DATABASE_URL` | The primary external database URL | ✅ | ❌ | Cannot be empty | +| `TURSO_AUTH_TOKEN` | Authentication token required to use Turso DB | ✅ | ❌ | Cannot be empty | +| `SERVER_PORT` | Default Server Port API hosted on | ❌ | `8080` | If set, must be a number between `0` and `65535` | +| `QUOTES_TABLE` | Name of Quotes Table | ❌ | `quotes` | Must be a string | +| `IMAGES_TABLE` | Name of Images Table | ❌ | `images` | Must be a string | +| `WEATHER_API_TOKEN` | OpenWeather API Token | ✅ | ❌ | Can't be empty string | + ## License -[MIT](LICENSE) + +This project is licensed under the [MIT License](http://opensource.org/licenses/MIT). diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..926a7c6 --- /dev/null +++ b/compose.yml @@ -0,0 +1,17 @@ +services: + dev: + build: + context: . + dockerfile: ./dev.Dockerfile + develop: + watch: + - action: sync + path: ./ + target: /app + ignore: [] + - action: sync+restart + path: .env + target: /app/.env + - action: rebuild + path: go.mod + env_file: .env \ No newline at end of file diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 0000000..21ac3be --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.23 + +RUN apt-get update && apt-get install -y gcc + +RUN useradd -ms /bin/sh -u 1001 app +USER app +WORKDIR /app + +COPY --chown=app:app go.mod go.sum ./ +RUN go mod download + +COPY --chown=app ./ ./ + +ENV SERVER_PORT=8080 +ENV CGO_ENABLED=1 + +CMD go run . + +EXPOSE 8080 \ No newline at end of file diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..9622d81 --- /dev/null +++ b/fly.toml @@ -0,0 +1,28 @@ +# fly.toml app configuration file generated for mue-api on 2024-09-02T02:12:33+01:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'mue-api' +primary_region = 'cdg' + +[build] + build-target = "runner" + +[http_service] + internal_port = 80 + force_https = true + auto_stop_machines = 'suspend' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[http_service.checks]] + grace_period = "5s" + interval = "30s" + method = "GET" + timeout = "5s" + path = "/" + +[[vm]] + size = 'shared-cpu-1x' \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d1326d8 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module mue-api + +go 1.23 + +require ( + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-playground/validator/v10 v10.22.0 + github.com/joho/godotenv v1.5.1 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/coder/websocket v1.8.12 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect + github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 // indirect + github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..386133f --- /dev/null +++ b/go.sum @@ -0,0 +1,56 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 h1:JclfLfqxOsICgvSNhF5W3cY2v5tylOtYJdTZKgPM8Bg= +github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go new file mode 100644 index 0000000..1102ae7 --- /dev/null +++ b/internal/handlers/image_handler.go @@ -0,0 +1,120 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "log" + "net/http" + "strings" + + "mue-api/internal/models" + + "github.com/go-chi/chi/v5" +) + +type ImageHandler struct { + DB *sql.DB + TableName string +} + +type Sizes struct { + ID string `json:"id"` + Label string `json:"label"` +} + +func (h *ImageHandler) GetImagePhotographers(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + photographers, err := models.GetImagePhotographers(ctx, h.DB, h.TableName) + if err != nil { + http.Error(w, "Failed to get photographers", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(photographers) +} + +func (h *ImageHandler) GetImageByID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "id") + image, err := models.GetImageByID(ctx, h.DB, h.TableName, id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(image) +} + +func (h *ImageHandler) GetImages(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + photographer := r.URL.Query().Get("photographer") + category := r.URL.Query().Get("category") + images, err := models.GetImages(ctx, h.DB, h.TableName, photographer, category) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(images) +} + +func (h *ImageHandler) GetImageCategories(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + categories, err := models.GetImageCategories(ctx, h.DB, h.TableName) + if err != nil { + http.Error(w, "Failed to get categories", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(categories) +} + +func (h *ImageHandler) GetImageSizes(w http.ResponseWriter, r *http.Request) { + sizes := []Sizes{ + {ID: "original", Label: "original"}, + {ID: "qhd", Label: "high"}, + {ID: "fhd", Label: "normal"}, + {ID: "hd", Label: "datasaver"}, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(sizes); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + queryParams := r.URL.Query() + + // Get categories from the query parameters + categories := queryParams["categories"] + photographers := queryParams["photographers"] + + var seenImages = models.GetCookieValueAsList(r, "seen_images") + + image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, categories, photographers) + if err != nil && strings.Contains(err.Error(), "no image found") { + // If no quotes are found, reset the seenQuotes list and try again + log.Println("No images found, resetting seenImages list") + seenImages = []string{} + models.SetCookie(w, "seen_images", "") + image, err = models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, categories, photographers) + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + seenImages = append(seenImages, image.ID) + seenImagesStr := strings.Join(seenImages, ",") + + models.SetCookie(w, "seen_images", seenImagesStr) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(image); err != nil { + log.Printf("Error encoding response: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// End of code diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go new file mode 100644 index 0000000..4c82689 --- /dev/null +++ b/internal/handlers/quote_handler.go @@ -0,0 +1,128 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "log" + "net/http" + "strings" + + "mue-api/internal/models" + + "github.com/go-chi/chi/v5" +) + +type QuoteHandler struct { + DB *sql.DB + TableName string +} + +func (h *QuoteHandler) GetQuoteLanguages(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + languages, err := models.GetQuoteLanguages(ctx, h.DB, h.TableName) + if err != nil { + http.Error(w, "Failed to get quote languages", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(languages) +} + +func (h *QuoteHandler) GetAllQuotes(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + language := r.URL.Query().Get("language") + quotes, err := models.GetAllQuotes(ctx, h.DB, h.TableName, language) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(quotes) +} + +func (h *QuoteHandler) GetQuoteByID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "id") + quote, err := models.GetQuoteByID(ctx, h.DB, h.TableName, id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(quote) +} + +func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + defaultLanguage := []string{"en"} + queryParams := r.URL.Query() + + languages := queryParams["language"] + authors := queryParams["quotes"] + + // If no language query parameter is provided, use the Accept-Language header + if len(languages) == 0 { + language := parseAcceptLanguage(r.Header.Get("Accept-Language")) + if language == "" { + languages = append(languages, "en") // Default to English if no Accept-Language header is present + } else { + languages = append(languages, parseAcceptLanguage(r.Header.Get("Accept-Language"))) + } + } + + // Retrieve the list of quote IDs from the client's cookies + var seenQuotes = models.GetCookieValueAsList(r, "seen_quotes") + + // Fetch a random quote with fallback to reset seenList and default to english + quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, languages, authors) + if err != nil && strings.Contains(err.Error(), "no quote found") { + log.Println("No quotes found, resetting seenQuotes list") + seenQuotes = []string{} + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, languages, authors) + if err != nil { + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, defaultLanguage, authors) + } + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + //Add the seen quote ID to the list of seen quotes + seenQuotes = append(seenQuotes, quote.ID) + seenQuotesStr := strings.Join(seenQuotes, ",") + + // Update the cookie with the new list of seen quote IDs + models.SetCookie(w, "seen_quotes", seenQuotesStr) + + // Return the quote as JSON + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(quote); err != nil { + log.Printf("Error encoding response: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// parseAcceptLanguage simplifies the Accept-Language header to a basic language code +func parseAcceptLanguage(header string) string { + if header == "" { + return "" + } + + // Split the header by commas to handle multiple languages + parts := strings.Split(header, ",") + if len(parts) > 0 { + // The first part should be the most preferred language + lang := strings.TrimSpace(parts[0]) + + // Split by ';' to remove any quality value (q=...) + if semicolonIdx := strings.Index(lang, ";"); semicolonIdx != -1 { + lang = lang[:semicolonIdx] + } + + // Return only the first 2 characters (e.g., "en-US" -> "en") + if len(lang) >= 2 { + return lang[:2] + } + } + return "" +} diff --git a/internal/handlers/root_handler.go b/internal/handlers/root_handler.go new file mode 100644 index 0000000..7e60627 --- /dev/null +++ b/internal/handlers/root_handler.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type Endpoint struct { + Method string `json:"method"` + Path string `json:"path"` + Description string `json:"description"` +} + +type ApiInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + QuoteEndpoints []Endpoint `json:"quote_endpoints"` + ImageEndpoints []Endpoint `json:"image_endpoints"` + WeatherEndpoints []Endpoint `json:"weather_endpoints"` + DocumentationURL string `json:"documentation_url"` +} + +// rootHandler handles requests to the root endpoint. +func RootHandler(w http.ResponseWriter, r *http.Request) { + apiInfo := ApiInfo{ + Name: "Mue API", + Version: "0.0.1", + Description: "This API provides quotes and images for the Mue Tab browser extension.", + QuoteEndpoints: []Endpoint{ + {Method: "GET", Path: "/quotes", Description: "Retrieve all quotes."}, + {Method: "GET", Path: "/quotes/{id}", Description: "Retrieve a specific quote by ID."}, + {Method: "GET", Path: "/quotes/random", Description: "Retrieve a random quote."}, + {Method: "GET", Path: "/quotes/language", Description: "Retrieve all available languages and the count of quotes in each."}, + }, + ImageEndpoints: []Endpoint{ + {Method: "GET", Path: "/images", Description: "Retrieve all images."}, + {Method: "GET", Path: "/images/{id}", Description: "Retrieve a specific image by ID."}, + {Method: "GET", Path: "/images/random", Description: "Retrieve a random image."}, + {Method: "GET", Path: "/images/photographers", Description: "Retrieve all photographers and the count of images by each."}, + {Method: "GET", Path: "/images/categories", Description: "Retrieve all image categories."}, + {Method: "GET", Path: "/images/sizes", Description: "Retrieve all image sizes."}, + }, + WeatherEndpoints: []Endpoint{ + {Method: "GET", Path: "/weather", Description: "Retrieve the weather for a specific location."}, + {Method: "GET", Path: "/weather/autolocation", Description: "Retrieve the city for the user's current location."}, + }, + DocumentationURL: "https://docs.muetab.com/", + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(apiInfo); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} diff --git a/internal/handlers/weather_handler.go b/internal/handlers/weather_handler.go new file mode 100644 index 0000000..57276cd --- /dev/null +++ b/internal/handlers/weather_handler.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "net/http" + + "mue-api/internal/models" +) + +type WeatherHandler struct { + DB *sql.DB + APIToken string +} + +func (h *WeatherHandler) GetLocationWeather(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + lat := r.URL.Query().Get("lat") + lon := r.URL.Query().Get("lon") + + weather, err := models.GetLocationWeather(ctx, lat, lon) + if err != nil { + http.Error(w, "Failed to get weather data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(weather) +} + +func (h *WeatherHandler) GetWeather(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + city := r.URL.Query().Get("city") + + weather, err := models.GetWeather(ctx, city) + if err != nil { + http.Error(w, "Failed to get weather data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(weather) +} diff --git a/internal/middleware/nocookie.go b/internal/middleware/nocookie.go new file mode 100644 index 0000000..c99b553 --- /dev/null +++ b/internal/middleware/nocookie.go @@ -0,0 +1,23 @@ +package middleware + +import "net/http" + +func NoCookieMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the 'no-cookie' query parameter is set to '1' + if r.URL.Query().Get("no-cookie") == "1" { + w = &noCookieResponseWriter{ResponseWriter: w} + r.Header.Del("Cookie") + } + next.ServeHTTP(w, r) + }) +} + +type noCookieResponseWriter struct { + http.ResponseWriter +} + +func (w *noCookieResponseWriter) WriteHeader(statusCode int) { + w.ResponseWriter.Header().Del("Set-Cookie") + w.ResponseWriter.WriteHeader(statusCode) +} diff --git a/internal/models/cookie.go b/internal/models/cookie.go new file mode 100644 index 0000000..cb679f4 --- /dev/null +++ b/internal/models/cookie.go @@ -0,0 +1,70 @@ +package models + +import ( + "log" + "net/http" + "strings" +) + +// Cookie Custom type to allow for defaults +type Cookie struct { + Name string + Value string + Path string `default:"/"` + MaxAge int `default:"0"` +} + +// Option allows for options when making cookies +type Option func(*Cookie) + +func SetCookie(w http.ResponseWriter, name string, value string, options ...Option) { + + //Creates a cookie with required and defaulted values + newCookie := &Cookie{ + Name: name, + Value: value, + } + + //Goes through the list of options inputted if any and changes the value + for _, option := range options { + option(newCookie) + } + + httpCookie := &http.Cookie{ + Name: newCookie.Name, + Value: newCookie.Value, + Path: newCookie.Path, + MaxAge: newCookie.MaxAge, + } + + http.SetCookie(w, httpCookie) +} + +//List of options that could be added +//Example use: models.SetCookie(w, "seen_quotes", seenQuotesStr, models.WithMaxAge(30)) + +func WithMaxAge(age int) Option { + return func(cookie *Cookie) { + cookie.MaxAge = age + } +} + +func WithPath(path string) Option { + return func(cookie *Cookie) { + cookie.Path = path + } +} + +func GetCookieValueAsList(r *http.Request, name string) []string { + cookie, err := r.Cookie(name) + + var returnString []string + if err == nil { + returnString = strings.Split(cookie.Value, ",") + } else { + // If the cookie does not exist, start with an empty list + returnString = []string{} + log.Println("No cookie found, starting with an empty list") + } + return returnString +} diff --git a/internal/models/image.go b/internal/models/image.go new file mode 100644 index 0000000..96f5394 --- /dev/null +++ b/internal/models/image.go @@ -0,0 +1,209 @@ +package models + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + "mue-api/internal/utils" +) + +type Image struct { + ID string `json:"id"` + Camera *string `json:"camera,omitempty"` + CreatedAt *string `json:"created_at,omitempty"` + LocationData *string `json:"location_data,omitempty"` + Photographer string `json:"photographer"` + Category string `json:"category"` + OriginalFileName string `json:"original_file_name"` + Colour string `json:"colour"` + PUN int `json:"pun"` + Version int `json:"version"` + BlurHash string `json:"blur_hash"` +} + +type PhotographerCount struct { + Photographer string + Count int +} + +type CategoryCount struct { + Category string + Count int +} + +func GetImagePhotographers(ctx context.Context, db *sql.DB, tableName string) ([]PhotographerCount, error) { + query := fmt.Sprintf(` + SELECT photographer, COUNT(*) as count + FROM %s + GROUP BY photographer + `, tableName) + + rows, err := db.QueryContext(ctx, query) + if err != nil { + log.Printf("Error querying image photographers: %v", err) + return nil, err + } + defer rows.Close() + + var photographers []PhotographerCount + for rows.Next() { + var lc PhotographerCount + if err := rows.Scan(&lc.Photographer, &lc.Count); err != nil { + log.Printf("Error scanning photographer count: %v", err) + return nil, err + } + photographers = append(photographers, lc) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return photographers, nil +} + +func GetImageCategories(ctx context.Context, db *sql.DB, tableName string) ([]CategoryCount, error) { + query := fmt.Sprintf(` + SELECT category, COUNT(*) as count + FROM %s + GROUP BY category +`, tableName) + + rows, err := db.QueryContext(ctx, query) + if err != nil { + log.Printf("Error querying image categories: %v", err) + return nil, err + } + defer rows.Close() + + var categories []CategoryCount + for rows.Next() { + var lc CategoryCount + if err := rows.Scan(&lc.Category, &lc.Count); err != nil { + log.Printf("Error scanning category count: %v", err) + return nil, err + } + categories = append(categories, lc) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return categories, nil +} + +func GetImageByID(ctx context.Context, db *sql.DB, tableName, id string) (Image, error) { + var image Image + query := fmt.Sprintf("SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM %s WHERE id = ?", tableName) + err := db.QueryRowContext(ctx, query, id).Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.Printf("No image found with id %s", id) + return image, errors.New("no image found") + } + log.Printf("Error querying imag by ID: %v", err) + return image, err + } + + return image, nil +} + +func GetImages(ctx context.Context, db *sql.DB, tableName, photographer, category string) ([]Image, error) { + query := fmt.Sprintf(` + SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash + FROM %s + `, tableName) + var args []interface{} + var conditions []string + + if photographer != "" { + conditions = append(conditions, "photographer = ?") + args = append(args, photographer) + } + if category != "" { + conditions = append(conditions, "category = ?") + args = append(args, category) + } + + if len(conditions) > 0 { + query += " WHERE " + conditions[0] + for i := 1; i < len(conditions); i++ { + query += " AND " + conditions[i] + } + } + + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + log.Printf("Error querying images: %v", err) + return nil, err + } + defer rows.Close() + + var images []Image + for rows.Next() { + var image Image + if err := rows.Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash); err != nil { + log.Printf("Error scanning image: %v", err) + return nil, err + } + images = append(images, image) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return images, nil +} + +func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, excludeIDs []string, includeCategories []string, includePhotographers []string) (*Image, error) { + //Creates an interface which is filled with the excluded values and a placeholder for whereConditions + var args []interface{} + var whereConditions []string + + //Builds the where clause dependent on what exclusions and inclusions apply + if len(excludeIDs) > 0 { + excludeClause := utils.BuildWhereClause("id NOT IN", excludeIDs, &args) + if excludeClause != "" { + whereConditions = append(whereConditions, excludeClause) + } + } + if len(includeCategories) > 0 { + categoryClause := utils.BuildWhereClause("category IN", includeCategories, &args) + if categoryClause != "" { + whereConditions = append(whereConditions, categoryClause) + } + } + if len(includePhotographers) > 0 { + photographerClause := utils.BuildWhereClause("photographer IN", includePhotographers, &args) + if photographerClause != "" { + whereConditions = append(whereConditions, photographerClause) + } + } + + whereClause := utils.CombineWhereClause(whereConditions) + + query := fmt.Sprintf(` + SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash + FROM %s + %s + ORDER BY RANDOM() + LIMIT 1 + `, tableName, whereClause) + + row := db.QueryRowContext(ctx, query, args...) + var image Image + if err := row.Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("no image found") + } + return nil, err + } + return &image, nil +} diff --git a/internal/models/quote.go b/internal/models/quote.go new file mode 100644 index 0000000..52b8e76 --- /dev/null +++ b/internal/models/quote.go @@ -0,0 +1,154 @@ +package models + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + "mue-api/internal/utils" +) + +// Quote represents a quote with its details. +type Quote struct { + ID string `json:"id"` + Quote string `json:"quote"` + Author *string `json:"author,omitempty"` + Occupation *string `json:"occupation,omitempty"` +} + +// LanguageCount represents a language and its quote count. +type LanguageCount struct { + Language string + Count int +} + +// GetQuoteLanguages returns the available languages and the count of quotes for each language. +func GetQuoteLanguages(ctx context.Context, db *sql.DB, tableName string) ([]LanguageCount, error) { + query := fmt.Sprintf(` + SELECT language, COUNT(*) as count + FROM %s + GROUP BY language + `, tableName) + + rows, err := db.QueryContext(ctx, query) + if err != nil { + log.Printf("Error querying quote languages: %v", err) + return nil, err + } + defer rows.Close() + + var languages []LanguageCount + for rows.Next() { + var lc LanguageCount + if err := rows.Scan(&lc.Language, &lc.Count); err != nil { + log.Printf("Error scanning language count: %v", err) + return nil, err + } + languages = append(languages, lc) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return languages, nil +} + +// GetAllQuotes fetches all quotes in the specified language. +func GetAllQuotes(ctx context.Context, db *sql.DB, tableName string, language string) ([]Quote, error) { + if language == "" { + language = "en" + } + + query := fmt.Sprintf("SELECT id, quote, author, author_occupation FROM %s WHERE language = ?", tableName) + rows, err := db.QueryContext(ctx, query, language) + if err != nil { + log.Printf("Error querying all quotes: %v", err) + return nil, err + } + defer rows.Close() + + var quotes []Quote + for rows.Next() { + var q Quote + if err := rows.Scan(&q.ID, &q.Quote, &q.Author, &q.Occupation); err != nil { + log.Printf("Error scanning quote: %v", err) + return nil, err + } + quotes = append(quotes, q) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return quotes, nil +} + +// GetQuoteByID fetches a quote by its ID in the specified language. +func GetQuoteByID(ctx context.Context, db *sql.DB, tableName string, id string) (Quote, error) { + var quote Quote + query := fmt.Sprintf("SELECT id, quote, author, author_occupation FROM %s WHERE id = ?", tableName) + err := db.QueryRowContext(ctx, query, id).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.Printf("No quote found with id %s", id) + return quote, errors.New("no quote found") + } + log.Printf("Error querying quote by ID: %v", err) + return quote, err + } + + return quote, nil +} + +func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, tableName string, excludeIDs []string, includeLanguages []string, includeAuthours []string) (*Quote, error) { + //Creates an interface which is filled with the excluded values and a placeholder for whereConditions + + var args []interface{} + var whereConditions []string + + //Builds the where clause dependent on what exclusions and inclusions apply + if len(excludeIDs) > 0 { + excludeClause := utils.BuildWhereClause("id NOT IN", excludeIDs, &args) + if excludeClause != "" { + whereConditions = append(whereConditions, excludeClause) + } + } + if len(includeLanguages) > 0 { + languagesClause := utils.BuildWhereClause("language IN", includeLanguages, &args) + if languagesClause != "" { + whereConditions = append(whereConditions, languagesClause) + } + } + if len(includeAuthours) > 0 { + authoursClause := utils.BuildWhereClause("author IN", includeAuthours, &args) + if authoursClause != "" { + whereConditions = append(whereConditions, authoursClause) + } + } + + whereClause := utils.CombineWhereClause(whereConditions) + + query := fmt.Sprintf(` + SELECT id, quote, author, author_occupation + FROM %s + %s + ORDER BY RANDOM() + LIMIT 1 + `, tableName, whereClause) + + row := db.QueryRowContext(ctx, query, args...) + var quote Quote + if err := row.Scan("e.ID, "e.Quote, "e.Author, "e.Occupation); err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("no quote found") + } + return nil, err + } + + return "e, nil +} diff --git a/internal/models/weather.go b/internal/models/weather.go new file mode 100644 index 0000000..17e9888 --- /dev/null +++ b/internal/models/weather.go @@ -0,0 +1,150 @@ +package models + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" +) + +type Weather struct { + Weather []struct { + Main string `json:"main"` + Description string `json:"description"` + Icon string `json:"icon"` + } `json:"weather"` + Main struct { + Temp float64 `json:"temp"` + TempMin float64 `json:"temp_min"` + TempMax float64 `json:"temp_max"` + FeelsLike float64 `json:"feels_like"` + Pressure int `json:"pressure"` + Humidity int `json:"humidity"` + } `json:"main"` + Visibility int `json:"visibility"` + Wind struct { + Speed float64 `json:"speed"` + Deg int `json:"deg"` + } `json:"wind"` + Clouds struct { + All int `json:"all"` + } `json:"clouds"` + ID int `json:"id"` +} + +type WeatherLocation struct { +} + +// get api token +var apiToken string = "c1b3b3b" + +// get weather data +func GetLocationWeather(ctx context.Context, lat, lon string) (*Weather, error) { + // build url + url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%s&lon=%s&appid=%s", lat, lon, apiToken) + + // create request + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + + if err != nil { + log.Printf("Error creating request: %v", err) + return nil, err + } + + // send request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.Printf("Error sending request: %v", err) + return nil, err + } + + // check response + if resp.StatusCode != http.StatusOK { + log.Printf("Error response: %v", resp.Status) + return nil, err + } + + // read response + defer resp.Body.Close() + + // parse response + var weather Weather + + if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil { + log.Printf("Error decoding response: %v", err) + return nil, err + } + + return &weather, nil +} + +func GetWeather(ctx context.Context, city string) (*Weather, error) { + // build url + url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s", city, apiToken) + + // create request + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + + if err != nil { + log.Printf("Error creating request: %v", err) + return nil, err + } + + // send request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.Printf("Error sending request: %v", err) + return nil, err + } + + // check response + if resp.StatusCode != http.StatusOK { + log.Printf("Error response: %v", resp.Status) + return nil, err + } + + // read response + defer resp.Body.Close() + + // parse response + var weather Weather + + if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil { + log.Printf("Error decoding response: %v", err) + return nil, err + } + + /* + weather: [{ + main: data.weather[0].main, + description: data.weather[0].description, + icon: data.weather[0].icon + }], + main: { + temp: data.main.temp, + temp_min: data.main.temp_min, + temp_max: data.main.temp_max, + temp_feels_like: data.main.feels_like, + pressure: data.main.pressure, + humidity: data.main.humidity + }, + visibility: data.visibility, + wind: { + speed: data.wind.speed, + deg: data.wind.deg + }, + clouds: { + all: data.clouds.all + }, + id: data.id + */ + + return &weather, nil +} diff --git a/internal/utils/database.go b/internal/utils/database.go new file mode 100644 index 0000000..556b512 --- /dev/null +++ b/internal/utils/database.go @@ -0,0 +1,60 @@ +package utils + +import ( + "database/sql" + "log" + + _ "github.com/mattn/go-sqlite3" + "github.com/sirupsen/logrus" + _ "github.com/tursodatabase/libsql-client-go/libsql" +) + +func ConnectDB(dbURL string, token string) (*sql.DB, error) { + url := dbURL + "?authToken=" + token + + db, err := sql.Open("libsql", url) + if err != nil { + return nil, err + } + + // Ping the database to ensure the connection is established + if err := db.Ping(); err != nil { + return nil, err + } + + return db, nil +} + +// InitDB ensures the connection to the database is established +func InitDB(db *sql.DB) { + // Check if the connection to the database is established + err := db.Ping() + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } +} + +// LoadAndConnectDB loads environment variables and connects to the database +func LoadAndConnectDB(dbURL string, token string) *sql.DB { + log := logrus.New() + + if dbURL == "" { + log.Fatalf("Database path is not set") + } + + if token == "" { + log.Fatalf("Token is not set") + } + + db, err := ConnectDB(dbURL, token) + if err != nil { + log.Fatalf("Failed to connect to database %s: %v", dbURL, err) + } + + log.WithFields(logrus.Fields{ + "dbPath": dbURL, + }).Info("Connected to database") + + InitDB(db) + return db +} diff --git a/internal/utils/env.go b/internal/utils/env.go new file mode 100644 index 0000000..342923f --- /dev/null +++ b/internal/utils/env.go @@ -0,0 +1,11 @@ +package utils + +import "os" + +// GetEnv retrieves an environment variable or returns a fallback value +func GetEnv(key, fallback string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return fallback +} diff --git a/internal/utils/sql.go b/internal/utils/sql.go new file mode 100644 index 0000000..7f02820 --- /dev/null +++ b/internal/utils/sql.go @@ -0,0 +1,28 @@ +package utils + +import ( + "fmt" + "strings" +) + +// BuildWhereClause creates a SQL where clause for requests, +// it takes the prefix (starting clause)then a list of items to iterate over and adds it to the list of args +func BuildWhereClause(prefix string, items []string, args *[]interface{}) string { + if len(items) == 0 { + return "" + } + placeholders := make([]string, len(items)) + for i, item := range items { + placeholders[i] = "?" + *args = append(*args, item) + } + return fmt.Sprintf("%s (%s)", prefix, strings.Join(placeholders, ",")) +} + +// CombineWhereClause combines a set of where clauses into one string +func CombineWhereClause(whereConditions []string) string { + if len(whereConditions) == 0 { + return "" + } + return "WHERE " + strings.Join(whereConditions, " AND ") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..993dbc5 --- /dev/null +++ b/main.go @@ -0,0 +1,199 @@ +package main + +import ( + "context" + "log" + "net/http" + "net/http/pprof" + "os" + "os/signal" + "syscall" + "time" + + "mue-api/internal/handlers" + local_middleware "mue-api/internal/middleware" + "mue-api/internal/utils" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-playground/validator/v10" + "github.com/joho/godotenv" +) + +// Config holds the application configuration +type Config struct { + LocalDatabase string `validate:"required"` + // DatabaseSyncInterval string `validate:"required,url"` + PrimaryDatabaseURL string `validate:"required,url"` + TursoAuthToken string `validate:"required"` + ServerPort string `validate:"required,numeric"` + QuotesTable string `validate:"required"` + ImagesTable string `validate:"required"` + WeatherAPIToken string `validate:"required"` +} + +// loadConfig loads configuration from environment variables +func loadConfig() (*Config, error) { + config := &Config{ + LocalDatabase: utils.GetEnv("DATABASE_CACHE_FILE", "replica.db"), + // DatabaseSyncInterval: utils.GetEnv("DATABASE_SYNC_INTERVAL", ""), + PrimaryDatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), + TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), + ServerPort: utils.GetEnv("SERVER_PORT", "8080"), + QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), + ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), + WeatherAPIToken: utils.GetEnv("WEATHER_API_TOKEN", ""), + } + + // Validate configuration + if err := validator.New().Struct(config); err != nil { + return nil, err + } + + return config, nil +} + +// main is the entry point of the application +func main() { + // Load environment variables from .env file + err := godotenv.Load() + if err != nil { + log.Println(err) + } + + // Load the configuration + config, err := loadConfig() + checkErr(err, "Invalid configuration") + + // Connect to db + db := utils.LoadAndConnectDB(config.PrimaryDatabaseURL, config.TursoAuthToken) + //checkErr(err, "Failed to connect to database") + defer db.Close() + + // Initialize handlers + quoteHandler := &handlers.QuoteHandler{DB: db, TableName: config.QuotesTable} + imageHandler := &handlers.ImageHandler{DB: db, TableName: config.ImagesTable} + weatherHandler := &handlers.WeatherHandler{DB: db, APIToken: config.WeatherAPIToken} + + // Setup router and middleware + r := setupRouter(quoteHandler, imageHandler, weatherHandler) + + // Start the server with graceful shutdown + startServer(r, config.ServerPort) +} + +// setupRouter configures the router and sub-routers +func setupRouter(quoteHandler *handlers.QuoteHandler, imageHandler *handlers.ImageHandler, weatherHandler *handlers.WeatherHandler) *chi.Mux { + r := chi.NewRouter() + + // Global middleware + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(local_middleware.NoCookieMiddleware) + + // Root and health check routes + r.Get("/", handlers.RootHandler) + r.Get("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // Mount sub-routers + r.Mount("/quotes", quotesRouter(quoteHandler)) + r.Mount("/images", imagesRouter(imageHandler)) + r.Mount("/weather", weatherRouter(weatherHandler)) + r.Mount("/debug", debugRouter()) + + return r +} + +// quotesRouter creates a sub-router for quote-related routes +func quotesRouter(handler *handlers.QuoteHandler) chi.Router { + r := chi.NewRouter() + r.Get("/", handler.GetAllQuotes) + r.Get("/random", handler.GetRandomQuote) + r.Get("/languages", handler.GetQuoteLanguages) + r.Route("/{id}", func(r chi.Router) { + r.Get("/", handler.GetQuoteByID) + // Add more quote-specific routes here (e.g., PUT, DELETE) + }) + return r +} + +// imagesRouter creates a sub-router for image-related routes +func imagesRouter(handler *handlers.ImageHandler) chi.Router { + r := chi.NewRouter() + r.Get("/", handler.GetImages) + r.Get("/photographers", handler.GetImagePhotographers) + r.Get("/random", handler.GetRandomImage) + r.Get("/sizes", handler.GetImageSizes) + r.Get("/categories", handler.GetImageCategories) + r.Route("/{id}", func(r chi.Router) { + r.Get("/", handler.GetImageByID) + // Add more image-specific routes here (e.g., PUT, DELETE) + }) + return r +} + +func weatherRouter(handler *handlers.WeatherHandler) chi.Router { + r := chi.NewRouter() + r.Get("/weather", handler.GetWeather) + r.Get("/location", handler.GetLocationWeather) + return r +} + +// debugRouter creates a sub-router for debugging routes like pprof +func debugRouter() chi.Router { + r := chi.NewRouter() + r.HandleFunc("/pprof/", pprof.Index) + r.HandleFunc("/pprof/cmdline", pprof.Cmdline) + r.HandleFunc("/pprof/profile", pprof.Profile) + r.HandleFunc("/pprof/symbol", pprof.Symbol) + r.HandleFunc("/pprof/trace", pprof.Trace) + return r +} + +// startServer initializes and starts the server with graceful shutdown +func startServer(handler http.Handler, port string) { + srv := &http.Server{ + Addr: ":" + port, + Handler: handler, + } + + // Start server in a goroutine + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start server: %v", err) + } + }() + log.Printf("Server started on :%s", port) + + // Graceful shutdown on interrupt signal + waitForShutdown(srv) +} + +// waitForShutdown handles graceful shutdown +func waitForShutdown(srv *http.Server) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exiting") +} + +// checkErr logs a fatal error if err is non-nil +func checkErr(err error, message string) { + if err != nil { + log.Fatalf("%s: %v", message, err) + } +} diff --git a/news.js b/news.js deleted file mode 100644 index 86c5b8a..0000000 --- a/news.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - date: '2022-07-20T21:14:38.000Z', - description: 'Mue 7.0 is now available!', - link: 'https://blog.muetab.com', - link_text: 'See what\'s new', - title: 'Mue 7.0', -}; \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 492c8f5..0000000 --- a/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "mue-api", - "private": true, - "description": "API for Mue, a new tab extension.", - "repository": { - "url": "github:mue/api" - }, - "homepage": "https://api.muetab.com", - "author": "The Mue Authors (https://github.com/mue/api/graphs/contributors)", - "license": "MIT", - "main": "src/", - "version": "1.7.0", - "scripts": { - "deploy": "wrangler deploy", - "start": "wrangler dev", - "dev": "wrangler dev", - "test": "jest", - "pretty": "prettier --write \"./src/**/*.{js,json}\"" - }, - "dependencies": { - "@baselime/edge-logger": "^0.2.2", - "@microlabs/otel-cf-workers": "1.0.0-rc.32", - "@opentelemetry/api": "^1.8.0", - "@supabase/supabase-js": "^2.0.5", - "cheerio": "1.0.0-rc.12", - "itty-router": "^2.6.6", - "itty-router-extras": "^0.4.6", - "node-fetch": "2.6.7", - "ua-parser-js": "1.0.32" - }, - "devDependencies": { - "eslint": "^8.27.0", - "eslint-config-prettier": "^8.5.0", - "prettier": "^3.2.5", - "wrangler": "^3.48.0" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 2eea875..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1910 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@baselime/edge-logger': - specifier: ^0.2.2 - version: 0.2.2 - '@microlabs/otel-cf-workers': - specifier: 1.0.0-rc.32 - version: 1.0.0-rc.32 - '@opentelemetry/api': - specifier: ^1.8.0 - version: 1.8.0 - '@supabase/supabase-js': - specifier: ^2.0.5 - version: 2.0.5 - cheerio: - specifier: 1.0.0-rc.12 - version: 1.0.0-rc.12 - itty-router: - specifier: ^2.6.6 - version: 2.6.6 - itty-router-extras: - specifier: ^0.4.6 - version: 0.4.6 - node-fetch: - specifier: 2.6.7 - version: 2.6.7 - ua-parser-js: - specifier: 1.0.32 - version: 1.0.32 - -devDependencies: - eslint: - specifier: ^8.27.0 - version: 8.27.0 - eslint-config-prettier: - specifier: ^8.5.0 - version: 8.5.0(eslint@8.27.0) - prettier: - specifier: ^3.2.5 - version: 3.2.5 - wrangler: - specifier: ^3.48.0 - version: 3.48.0 - -packages: - - /@baselime/edge-logger@0.2.2: - resolution: {integrity: sha512-7i50PawqZDLFMzWcFKOoWZeb7AcuHdDwiiiXCUdFSjB3NkCJKO4ZxevUXFR3yL82ROSvrNDz83NEDpxyBx78eA==} - dev: false - - /@cloudflare/kv-asset-handler@0.3.1: - resolution: {integrity: sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==} - dependencies: - mime: 3.0.0 - dev: true - - /@cloudflare/workerd-darwin-64@1.20240404.0: - resolution: {integrity: sha512-rc/ov3I9GwgKRtUnkShNW3TIoZEPHzExrMRNlHq1VpXQRBSchHdMw8meMn54+oqgxW1AKLmPWj/c0A7EnYAsIw==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-darwin-arm64@1.20240404.0: - resolution: {integrity: sha512-V9oPjeC2PYBCoAYnjbO2bsjT7PtzxfUHnh780FUi1r59Hwxd7FNlojwsIidA0nS/1WV5UKeJusIdrYlQbsketA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-linux-64@1.20240404.0: - resolution: {integrity: sha512-ndO7q6G2X8uYd5byGFzow4SWPqINQcmJ7pKKATNa+9vh/YMO0of2ihPntnm9uv577l8nRiAwRkHbnsWoEI33qQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-linux-arm64@1.20240404.0: - resolution: {integrity: sha512-hto5pjKYFqFu2Rvxnh84TzgDwalBRXQSwOVHltcgqo48en9TJDCN48ZtLj2G7QTGUOMW88h+nqdbj8+P7S/GXQ==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-windows-64@1.20240404.0: - resolution: {integrity: sha512-DpCLvNkOeFinKGJwv9qbyT7RLZ1168dhPx85IHSqAYVWZIszNSmNOkEDqklvoJoab01AqETrrEhwBdmjCA7qfA==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - - /@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19): - resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} - peerDependencies: - esbuild: '*' - dependencies: - esbuild: 0.17.19 - dev: true - - /@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19): - resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} - peerDependencies: - esbuild: '*' - dependencies: - esbuild: 0.17.19 - escape-string-regexp: 4.0.0 - rollup-plugin-node-polyfills: 0.2.1 - dev: true - - /@esbuild/android-arm64@0.17.19: - resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.17.19: - resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.17.19: - resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.17.19: - resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.17.19: - resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.17.19: - resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.17.19: - resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.17.19: - resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.17.19: - resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.17.19: - resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.17.19: - resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.17.19: - resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.17.19: - resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.17.19: - resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.17.19: - resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.17.19: - resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.17.19: - resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.17.19: - resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.17.19: - resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.17.19: - resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.17.19: - resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.17.19: - resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@eslint/eslintrc@1.3.3: - resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.4.1 - globals: 13.17.0 - ignore: 5.2.0 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@fastify/busboy@2.1.0: - resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} - engines: {node: '>=14'} - dev: true - - /@humanwhocodes/config-array@0.11.7: - resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/object-schema@1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - dev: true - - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true - - /@jridgewell/trace-mapping@0.3.9: - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - - /@microlabs/otel-cf-workers@1.0.0-rc.32: - resolution: {integrity: sha512-G+sjxHPmw0ndqGvR7WdIY+61t7UhAUfZ+dv1WL/mji3vRpG67zHciXV4Hjibr34KRzGn0wQp9MsZVaQ9x2fWcQ==} - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/exporter-trace-otlp-http': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-exporter-base': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-transformer': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.23.0 - ts-checked-fsm: 1.1.0 - dev: false - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.13.0 - dev: true - - /@opentelemetry/api-logs@0.43.0: - resolution: {integrity: sha512-0CXMOYPXgAdLM2OzVkiUfAL6QQwWVhnMfUXCqLsITY42FZ9TxAhZIHkoc4mfVxvPuXsBnRYGR8UQZX86p87z4A==} - engines: {node: '>=14'} - dependencies: - '@opentelemetry/api': 1.8.0 - dev: false - - /@opentelemetry/api@1.6.0: - resolution: {integrity: sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g==} - engines: {node: '>=8.0.0'} - dev: false - - /@opentelemetry/api@1.8.0: - resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} - engines: {node: '>=8.0.0'} - dev: false - - /@opentelemetry/core@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/semantic-conventions': 1.17.0 - dev: false - - /@opentelemetry/core@1.23.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/semantic-conventions': 1.23.0 - dev: false - - /@opentelemetry/exporter-trace-otlp-http@0.43.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-X6RGl4RTWC13EBrFstAbTh4vKqVqf6afpvFcud9qYhvl2A53OZ5RTAQP+9MrAMhthiKQaftNsEDdB2/0Sq+Xkw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-exporter-base': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-transformer': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-trace-base': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/otlp-exporter-base@0.43.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-LXNtRFVuPRXB9q0qdvrLikQ3NtT9Jmv255Idryz3RJPhOh/Fa03sBASQoj3D55OH3xazmA90KFHfhJ/d8D8y4A==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/otlp-transformer@0.43.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-KXYmgzWdVBOD5NvPmGW1nEMJjyQ8gK3N8r6pi4HvmEhTp0v4T13qDSax4q0HfsqmbPJR355oqQSJUnu1dHNutw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/api-logs': 0.43.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-logs': 0.43.0(@opentelemetry/api-logs@0.43.0)(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-metrics': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-trace-base': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/resources@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.17.0 - dev: false - - /@opentelemetry/resources@1.23.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.23.0 - dev: false - - /@opentelemetry/sdk-logs@0.43.0(@opentelemetry/api-logs@0.43.0)(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-JyJ2BBRKm37Mc4cSEhFmsMl5ASQn1dkGhEWzAAMSlhPtLRTv5PfvJwhR+Mboaic/eDLAlciwsgijq8IFlf6IgQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.4.0 <1.7.0' - '@opentelemetry/api-logs': '>=0.39.1' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/api-logs': 0.43.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/sdk-metrics@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-HlWM27yGmYuwCoVRe3yg2PqKnIsq0kEF0HQgvkeDWz2NYkq9fFaSspR6kvjxUTbghAlZrabiqbgyKoYpYaXS3w==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - lodash.merge: 4.6.2 - dev: false - - /@opentelemetry/sdk-trace-base@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.17.0 - dev: false - - /@opentelemetry/sdk-trace-base@1.23.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-PzBmZM8hBomUqvCddF/5Olyyviayka44O5nDWq673np3ctnvwMOvNrsUORZjKja1zJbwEuD9niAGbnVrz3jwRQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.23.0 - dev: false - - /@opentelemetry/semantic-conventions@1.17.0: - resolution: {integrity: sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA==} - engines: {node: '>=14'} - dev: false - - /@opentelemetry/semantic-conventions@1.23.0: - resolution: {integrity: sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==} - engines: {node: '>=14'} - dev: false - - /@supabase/functions-js@2.0.0: - resolution: {integrity: sha512-ozb7bds2yvf5k7NM2ZzUkxvsx4S4i2eRKFSJetdTADV91T65g4gCzEs9L3LUXSrghcGIkUaon03VPzOrFredqg==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/gotrue-js@2.2.3: - resolution: {integrity: sha512-2pw29sk2Fv/6CHFd72bIGTUNxI3UsyaEvjdpvgvvFtX7DmVBZPuotmQaZ56/V0sKREBdglt+bwIjOece1lmDCw==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/postgrest-js@1.1.0: - resolution: {integrity: sha512-qkY8TqIu5sJuae8gjeDPjEqPrefzcTraW9PNSVJQHq4TEv98ZmwaXGwBGz0bVL63bqrGA5hqREbQHkANUTXrvA==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/realtime-js@2.1.0: - resolution: {integrity: sha512-iplLCofTeYjnx9FIOsIwHLhMp0+7UVyiA4/sCeq40VdOgN9eTIhjEno9Tgh4dJARi4aaXoKfRX1DTxgZaOpPAw==} - dependencies: - '@types/phoenix': 1.5.4 - websocket: 1.0.34 - transitivePeerDependencies: - - supports-color - dev: false - - /@supabase/storage-js@2.0.0: - resolution: {integrity: sha512-7kXThdRt/xqnOOvZZxBqNkeX1CFNUWc0hYBJtNN/Uvt8ok9hD14foYmroWrHn046wEYFqUrB9U35JYsfTrvltA==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/supabase-js@2.0.5: - resolution: {integrity: sha512-zewk3U8wSe2UlKqzUk7iNxeMZzgDa5kjosSG4SKuAizIv+xzS+c2EZsYnNtc2byGjah4QT1dw3t91k2E+j+Akg==} - dependencies: - '@supabase/functions-js': 2.0.0 - '@supabase/gotrue-js': 2.2.3 - '@supabase/postgrest-js': 1.1.0 - '@supabase/realtime-js': 2.1.0 - '@supabase/storage-js': 2.0.0 - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - - /@types/phoenix@1.5.4: - resolution: {integrity: sha512-L5eZmzw89eXBKkiqVBcJfU1QGx9y+wurRIEgt0cuLH0hwNtVUxtx+6cu0R2STwWj468sjXyBYPYDtGclUd1kjQ==} - dev: false - - /acorn-jsx@5.3.2(acorn@8.8.1): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.8.1 - dev: true - - /acorn-walk@8.3.1: - resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} - engines: {node: '>=0.4.0'} - dev: true - - /acorn@8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /anymatch@3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /as-table@1.0.55: - resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} - dependencies: - printable-characters: 1.0.42 - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true - - /blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - dev: true - - /boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: false - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /bufferutil@4.0.7: - resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} - engines: {node: '>=6.14.2'} - requiresBuild: true - dependencies: - node-gyp-build: 4.5.0 - dev: false - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /capnp-ts@0.7.0: - resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} - dependencies: - debug: 4.3.4 - tslib: 2.6.2 - transitivePeerDependencies: - - supports-color - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.0.1 - dev: false - - /cheerio@1.0.0-rc.12: - resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} - engines: {node: '>= 6'} - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.0.1 - htmlparser2: 8.0.1 - parse5: 7.1.1 - parse5-htmlparser2-tree-adapter: 7.0.0 - dev: false - - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.2 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: true - - /cross-fetch@3.1.5: - resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} - dependencies: - node-fetch: 2.6.7 - transitivePeerDependencies: - - encoding - dev: false - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.0.1 - nth-check: 2.1.1 - dev: false - - /css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - dev: false - - /d@1.0.1: - resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} - dependencies: - es5-ext: 0.10.62 - type: 1.2.0 - dev: false - - /data-uri-to-buffer@2.0.2: - resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} - dev: true - - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - dev: false - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.4.0 - dev: false - - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: false - - /domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - dependencies: - domelementtype: 2.3.0 - dev: false - - /domutils@3.0.1: - resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dev: false - - /entities@4.4.0: - resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} - engines: {node: '>=0.12'} - dev: false - - /es5-ext@0.10.62: - resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==} - engines: {node: '>=0.10'} - requiresBuild: true - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.3 - next-tick: 1.1.0 - dev: false - - /es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - dependencies: - d: 1.0.1 - es5-ext: 0.10.62 - es6-symbol: 3.1.3 - dev: false - - /es6-symbol@3.1.3: - resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} - dependencies: - d: 1.0.1 - ext: 1.7.0 - dev: false - - /esbuild@0.17.19: - resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.17.19 - '@esbuild/android-arm64': 0.17.19 - '@esbuild/android-x64': 0.17.19 - '@esbuild/darwin-arm64': 0.17.19 - '@esbuild/darwin-x64': 0.17.19 - '@esbuild/freebsd-arm64': 0.17.19 - '@esbuild/freebsd-x64': 0.17.19 - '@esbuild/linux-arm': 0.17.19 - '@esbuild/linux-arm64': 0.17.19 - '@esbuild/linux-ia32': 0.17.19 - '@esbuild/linux-loong64': 0.17.19 - '@esbuild/linux-mips64el': 0.17.19 - '@esbuild/linux-ppc64': 0.17.19 - '@esbuild/linux-riscv64': 0.17.19 - '@esbuild/linux-s390x': 0.17.19 - '@esbuild/linux-x64': 0.17.19 - '@esbuild/netbsd-x64': 0.17.19 - '@esbuild/openbsd-x64': 0.17.19 - '@esbuild/sunos-x64': 0.17.19 - '@esbuild/win32-arm64': 0.17.19 - '@esbuild/win32-ia32': 0.17.19 - '@esbuild/win32-x64': 0.17.19 - dev: true - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /eslint-config-prettier@8.5.0(eslint@8.27.0): - resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.27.0 - dev: true - - /eslint-scope@7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-utils@3.0.0(eslint@8.27.0): - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.27.0 - eslint-visitor-keys: 2.1.0 - dev: true - - /eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - dev: true - - /eslint-visitor-keys@3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint@8.27.0: - resolution: {integrity: sha512-0y1bfG2ho7mty+SiILVf9PfuRA49ek4Nc60Wmmu62QlobNR+CeXa4xXIJgcuwSQgZiWaPH+5BDsctpIW0PR/wQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint/eslintrc': 1.3.3 - '@humanwhocodes/config-array': 0.11.7 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0(eslint@8.27.0) - eslint-visitor-keys: 3.3.0 - espree: 9.4.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.17.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.0 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-sdsl: 4.1.5 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@9.4.1: - resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2(acorn@8.8.1) - eslint-visitor-keys: 3.3.0 - dev: true - - /esquery@1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - - /estree-walker@0.6.1: - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - - /exit-hook@2.2.1: - resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} - engines: {node: '>=6'} - dev: true - - /ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - dependencies: - type: 2.7.2 - dev: false - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - - /fastq@1.13.0: - resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} - dependencies: - reusify: 1.0.4 - dev: true - - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flat-cache: 3.0.4 - dev: true - - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - dev: true - - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - dev: true - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true - - /get-source@2.0.12: - resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} - dependencies: - data-uri-to-buffer: 2.0.2 - source-map: 0.6.1 - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: true - - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /globals@13.17.0: - resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 - dev: true - - /htmlparser2@8.0.1: - resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.0.1 - entities: 4.4.0 - dev: false - - /ignore@5.2.0: - resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} - engines: {node: '>= 4'} - dev: true - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - dependencies: - hasown: 2.0.2 - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true - - /is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - dev: false - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /itty-router-extras@0.4.6: - resolution: {integrity: sha512-6r7HQBkFMPSJfcKksrKC7avEQnPCSSEvoz6PAAZMNhz8hthYu1pzedXDrvTFDWXJosfuaittzoNciWHO/TxMaw==} - dev: false - - /itty-router@2.6.6: - resolution: {integrity: sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g==} - dev: false - - /js-sdsl@4.1.5: - resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - /magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - dependencies: - sourcemap-codec: 1.4.8 - dev: true - - /mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - dev: true - - /miniflare@3.20240404.0: - resolution: {integrity: sha512-+FOTcztPMW3akmucX4vE0PWMNvP4JBwl4s9ieA84fcOaDtTbtfU1rHXpcacj16klpUpvSnD6xd8Sjsn6SJXPfg==} - engines: {node: '>=16.13'} - hasBin: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - acorn: 8.8.1 - acorn-walk: 8.3.1 - capnp-ts: 0.7.0 - exit-hook: 2.2.1 - glob-to-regexp: 0.4.1 - stoppable: 1.1.0 - undici: 5.28.2 - workerd: 1.20240404.0 - ws: 8.11.0 - youch: 3.3.3 - zod: 3.22.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: false - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - dev: true - - /nanoid@3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - - /next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - dev: false - - /node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - - /node-forge@1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} - engines: {node: '>= 6.13.0'} - dev: true - - /node-gyp-build@4.5.0: - resolution: {integrity: sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==} - hasBin: true - dev: false - - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - dependencies: - boolbase: 1.0.0 - dev: false - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: true - - /optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.3 - dev: true - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} - dependencies: - domhandler: 5.0.3 - parse5: 7.1.1 - dev: false - - /parse5@7.1.1: - resolution: {integrity: sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==} - dependencies: - entities: 4.4.0 - dev: false - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true - - /path-to-regexp@6.2.1: - resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - dev: true - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /printable-characters@1.0.42: - resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} - dev: true - - /punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - - /regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /resolve.exports@2.0.2: - resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} - engines: {node: '>=10'} - dev: true - - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /rollup-plugin-inject@3.0.2: - resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} - deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. - dependencies: - estree-walker: 0.6.1 - magic-string: 0.25.9 - rollup-pluginutils: 2.8.2 - dev: true - - /rollup-plugin-node-polyfills@0.2.1: - resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} - dependencies: - rollup-plugin-inject: 3.0.2 - dev: true - - /rollup-pluginutils@2.8.2: - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - dependencies: - estree-walker: 0.6.1 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /selfsigned@2.1.1: - resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} - engines: {node: '>=10'} - dependencies: - node-forge: 1.3.1 - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true - - /sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - dev: true - - /stacktracey@2.1.8: - resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} - dependencies: - as-table: 1.0.55 - get-source: 2.0.12 - dev: true - - /stoppable@1.1.0: - resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} - engines: {node: '>=4', npm: '>=6'} - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - - /ts-checked-fsm@1.1.0: - resolution: {integrity: sha512-kYbLNv4/orTm6TA0mdQmDy3k9teMmhyvzwtgYo3a0/TFjwDXi/g/3LkJlQd99fqqH5NxhWYLanNrwovb/r9PWA==} - dev: false - - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - - /type@1.2.0: - resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} - dev: false - - /type@2.7.2: - resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} - dev: false - - /typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - dependencies: - is-typedarray: 1.0.0 - dev: false - - /ua-parser-js@1.0.32: - resolution: {integrity: sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==} - dev: false - - /undici@5.28.2: - resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} - engines: {node: '>=14.0'} - dependencies: - '@fastify/busboy': 2.1.0 - dev: true - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.1.1 - dev: true - - /utf-8-validate@5.0.10: - resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} - engines: {node: '>=6.14.2'} - requiresBuild: true - dependencies: - node-gyp-build: 4.5.0 - dev: false - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false - - /websocket@1.0.34: - resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==} - engines: {node: '>=4.0.0'} - dependencies: - bufferutil: 4.0.7 - debug: 2.6.9 - es5-ext: 0.10.62 - typedarray-to-buffer: 3.1.5 - utf-8-validate: 5.0.10 - yaeti: 0.0.6 - transitivePeerDependencies: - - supports-color - dev: false - - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} - dev: true - - /workerd@1.20240404.0: - resolution: {integrity: sha512-U4tfnvBcPMsv7pmRGuF0J5UnoZi6tbc64tXNfyijI74r6w6Vlb2+a6eibdQL8g0g46+4vjuTKME9G5RvSvdc8g==} - engines: {node: '>=16'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240404.0 - '@cloudflare/workerd-darwin-arm64': 1.20240404.0 - '@cloudflare/workerd-linux-64': 1.20240404.0 - '@cloudflare/workerd-linux-arm64': 1.20240404.0 - '@cloudflare/workerd-windows-64': 1.20240404.0 - dev: true - - /wrangler@3.48.0: - resolution: {integrity: sha512-Wv7JS6FyX1j9HkaM6WL3fmTzBMAYc4hPSyZCuxuH55hkJDX/7ts+YAgsaN1U8rKoDrV3FVSgBfI9TyqP9iuM8Q==} - engines: {node: '>=16.17.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20240404.0 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - dependencies: - '@cloudflare/kv-asset-handler': 0.3.1 - '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) - '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) - blake3-wasm: 2.1.5 - chokidar: 3.5.3 - esbuild: 0.17.19 - miniflare: 3.20240404.0 - nanoid: 3.3.4 - path-to-regexp: 6.2.1 - resolve: 1.22.8 - resolve.exports: 2.0.2 - selfsigned: 2.1.1 - source-map: 0.6.1 - xxhash-wasm: 1.0.2 - optionalDependencies: - fsevents: 2.3.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /ws@8.11.0: - resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - - /xxhash-wasm@1.0.2: - resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} - dev: true - - /yaeti@0.0.6: - resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} - engines: {node: '>=0.10.32'} - dev: false - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true - - /youch@3.3.3: - resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==} - dependencies: - cookie: 0.5.0 - mustache: 4.2.0 - stacktracey: 2.1.8 - dev: true - - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - dev: true diff --git a/public/assets/mue-api-logo-banner.png b/public/assets/mue-api-logo-banner.png new file mode 100644 index 0000000..e16d0bf Binary files /dev/null and b/public/assets/mue-api-logo-banner.png differ diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..215d808 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-go:latest diff --git a/script.js b/script.js new file mode 100644 index 0000000..b6e7887 --- /dev/null +++ b/script.js @@ -0,0 +1,20 @@ +import http from "k6/http"; +import { sleep } from "k6"; + +export let options = { + vus: 25000, // virtual users + duration: "10s", // duration of the test +}; + +export default function () { + let res = http.get("http://localhost:8080/quotes/random"); + + // Log the response body to the console + console.log("Response body: " + res.body); + + // Optional: Log the status code to the console + console.log("Status code: " + res.status); + + // Sleep for 1 second between requests + sleep(1); +} diff --git a/src/handler.js b/src/handler.js deleted file mode 100644 index 3208690..0000000 --- a/src/handler.js +++ /dev/null @@ -1,91 +0,0 @@ -import router from './router'; -import { json, error } from 'itty-router-extras'; -import { trace } from '@opentelemetry/api'; -import { BaselimeLogger } from '@baselime/edge-logger'; -import { createClient } from '@supabase/supabase-js'; - -export default { - /** - * @param {Request} req - */ - async fetch(req, env, ctx) { - const requestId = req.headers.get('cf-ray') || crypto.randomUUID(); - const span = trace.getActiveSpan(); - span?.setAttribute('cf-ray', requestId); - const logger = new BaselimeLogger({ - apiKey: env.BASELIME_API_KEY, - ctx, - dataset: 'cloudflare', - // isLocalDev: env.IS_LOCAL_MODE, - namespace: 'api', - requestId, - service: 'mue', - }); - ctx.$logger = logger; - ctx.$supabase = createClient(env.SUPABASE_URL, env.SUPABASE_TOKEN); - logger.info('Request', { - method: req.method, - origin: req.headers.get('origin'), - query: req.query, - requestId, - url: req.url, - }); - try { - const cache = caches.default; - const cacheKey = new Request(new URL(req.url).toString(), req); - - // attempt to serve from cache - if (req.method === 'GET') { - const res = await cache.match(cacheKey); - if (res) return res; - } - - // if not cached - - /** @type {Response} */ - let res = await router.handle(req, env, ctx); - - if (!(res instanceof Response)) { - res = json(res, { - // cdn 1d, client 1h, stale 1h - headers: { 'Cache-Control': 'public, s-max-age=86400, max-age=3600, stale-while-revalidate=3600' }, - }); - } - - res.headers.set('Access-Control-Allow-Origin', '*'); - - if (res.status > 399) { - // !res.ok - logger.warn('Non-ok response', { - request: { - method: req.method, - query: req.query, - url: req.url, - }, - response: { - body: await res.clone().text(), - status: res.status, - }, - }); - } - - - if (res.headers.has('Cache-Control') && res.headers.get('Cache-Control') !== 'no-store') { - ctx.waitUntil(cache.put(cacheKey, res.clone())); - } - - return res; - } catch (err) { - logger.error('Internal Serverless Error', { error: JSON.stringify(err) }); - const res = error(500, { - error: err?.message, - message: 'Internal Serverless Error', - stack: err?.stack, - }); - res.headers.set('Access-Control-Allow-Origin', '*'); - return res; - } finally { - ctx.waitUntil(logger.flush()); - } - }, -}; \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index d2fa7aa..0000000 --- a/src/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import { instrument } from '@microlabs/otel-cf-workers'; -import handler from './handler'; - -const config = (env) => ({ - exporter: { - headers: { 'x-api-key': env.BASELIME_API_KEY }, - url: 'https://otel.baselime.io/v1', - }, - service: { name: 'mue' }, -}); - -export default instrument(handler, config); \ No newline at end of file diff --git a/src/pagination.js b/src/pagination.js deleted file mode 100644 index 14590ec..0000000 --- a/src/pagination.js +++ /dev/null @@ -1,13 +0,0 @@ -import { error } from 'itty-router-extras'; - -export default function paginate(data, query) { - if (query.offset) { - if (!query.limit) { - return error(400, 'Page limit is required for pagination'); - } - const limit = parseInt(query.limit); - const offset = parseInt(query.offset); - data = data.slice(offset, offset + limit); - } - return data; -} \ No newline at end of file diff --git a/src/router.js b/src/router.js deleted file mode 100644 index 3b7f496..0000000 --- a/src/router.js +++ /dev/null @@ -1,68 +0,0 @@ -import { Router } from 'itty-router'; -import { json, error } from 'itty-router-extras'; -import v2 from './v2'; -import sizes from './sizes'; -import news from '../news'; -import { getCollection, getCollections, getFeatured, getItem, getItems } from './v2/marketplace'; - -export default Router() - .get('/', () => 'Hello World! API docs: https://docs.muetab.com/api/introduction') - .get('/collection/:collection', getCollection) - .get('/collections', getCollections) - .get('/featured', getFeatured) - .get('/images/categories', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - return data.map((row) => row.name); - }) - .get('/images/photographers', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_photographers'); - return data.map((row) => row.name); - }) - .get('/images/random', async (req, env, ctx) => { - const kv_id = 'image_categories'; - let categories = await env.cache.get(kv_id, { - cacheTtl: 3600, // cache at this location for an hour - type: 'json', - }); - if (!categories) { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - // save for 1 day - ctx.waitUntil(env.cache.put(kv_id, JSON.stringify(data), { expirationTtl: 86400 })); - categories = data; - } - const category = categories[Math.floor(Math.random() * categories.length)].name; - const { data } = await ctx.$supabase.rpc('get_random_image', { _category: category }).single(); - const format = req.headers.get('accept')?.includes('avif') ? 'avif' : 'webp'; - const quality = sizes[req.query?.quality] ?? 'fhd'; - return json( - { - camera: data.camera, - category: data.category, - file: `https://cdn.muetab.com/img/${quality}/${data.id}.${format}?v=${data.version}`, - location: data.location_name, - photographer: data.photographer, - }, - { headers: { 'Cache-Control': 'no-store' } }, - ); - }) - .get('/item/:category/:item', getItem) - .get('/items/:category', getItems) - .get('/news', () => ({ news })) - .get('/quotes/languages', () => ['English', 'French']) - .get('/quotes/random', async (req, env, ctx) => { - const language = req.query.language?.replace('French', 'Français') || 'English'; - const { data } = await ctx.$supabase - .rpc('get_random_old_quote', { _language: language }) - .single(); - return data; - }) - .get('/stats', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .get('/versions', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .all('/v2/*', v2.handle) - .all('*', () => error(404, 'Not Found')); diff --git a/src/sizes.js b/src/sizes.js deleted file mode 100644 index ad81387..0000000 --- a/src/sizes.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - original: 'original', - high: 'qhd', // eslint-disable-line sort-keys - normal: 'fhd', - datasaver: 'hd', // eslint-disable-line sort-keys -}; diff --git a/src/v2/index.js b/src/v2/index.js deleted file mode 100644 index 43ac603..0000000 --- a/src/v2/index.js +++ /dev/null @@ -1,186 +0,0 @@ -import { Router } from 'itty-router'; -import { json, error } from 'itty-router-extras'; -import sizes from '../sizes'; -import news from '../../news'; -import { getUnsplashImage } from './unsplash'; -import { getPexelsImage } from './pexels'; -import { withWeatherLanguage } from './weather'; -import { - getCollection, - getCollections, - getCurator, - getCurators, - getFeatured, - getItem, - getItems, -} from './marketplace'; - -export default Router({ base: '/v2' }) - .get('/marketplace/collection/:collection', getCollection) - .get('/marketplace/collections', getCollections) - .get('/marketplace/curator/:curator', getCurator) - .get('/marketplace/curators', getCurators) - .get('/marketplace/featured', getFeatured) - .get('/marketplace/item/:category/:item', getItem) - .get('/marketplace/items/:category', getItems) - .get('/gps', withWeatherLanguage, async (req, env, ctx) => { - const { latitude, longitude } = req.query; - if (!latitude || !longitude) { - return error(400, '`latitude` and `longitude` params are required'); - } - const url = `https://api.openweathermap.org/geo/1.0/reverse?lat=${latitude}&lon=${longitude}&limit=1&appid=${env.OPENWEATHER_TOKEN}&lang=${ctx.$language}`; - const data = await (await fetch(url)).json(); - return json(data, { - headers: { - // 1w, stale 1d - 'Cache-Control': 'public, max-age=604800, stale-while-revalidate=86400, immutable', - }, - }); - }) - .get('/images/categories', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - return data; - }) - .get('/images/pexels', async (req, ...rest) => { - const data = await getPexelsImage(req.query.quality ?? 'normal', ...rest); - return json(data, { headers: { 'Cache-Control': 'no-store' } }); - }) - .get('/images/photographers', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_photographers'); - return data; - }) - .get('/images/random', async (req, env, ctx) => { - const kv_id = 'v2_image_categories'; - let allowed = await env.cache.get(kv_id, { - cacheTtl: 3600, // cache at this location for an hour - type: 'json', - }); - if (!allowed) { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - // save for 1 day - ctx.waitUntil(env.cache.put(kv_id, JSON.stringify(data), { expirationTtl: 86400 })); - allowed = data; - } - allowed = allowed.map((row) => row.name); - let categories = - req.query.categories?.split(',')?.filter((category) => allowed.includes(category)) ?? []; - if (categories.length === 0) categories = allowed; - const category = categories[Math.floor(Math.random() * categories.length)]; - const { data } = await ctx.$supabase - .rpc('get_random_image', { - _category: category, - _exclude: req.query.exclude, - }) - .single(); - const format = req.headers.get('accept')?.includes('avif') ? 'avif' : 'webp'; - const quality = sizes[req.query?.quality] ?? 'fhd'; - const coordinates = data.location_data?.split(','); - return json( - { - blur_hash: data.blur_hash, - camera: data.camera, - category: data.category, - colour: data.colour, - file: `https://cdn.muetab.com/img/${quality}/${data.id}.${format}?v=${data.version}`, - id: data.id, - location: { - latitude: coordinates?.[0] ?? null, - longitude: coordinates?.[1] ?? null, - name: data.location_name, - }, - photographer: data.photographer, - pun: data.pun, - }, - { headers: { 'Cache-Control': 'no-store' } }, - ); - }) - .get('/images/unsplash', async (req, ...rest) => { - const named_collections = { - animals: 'nJDnd_8TN_g', - architecture: 'e9-QAhrwZ5Q', - landscapes: 'SxeKQtPuR0U', - plants: 'y15m5OvaD98', - }; - const { categories, collections, orientation, topics, username } = req.query; - const unsplash_query = new URLSearchParams({ orientation: orientation ?? 'landscape' }); - if (categories && categories.length > 0) { - unsplash_query.set('collections', categories - .split(',') - .map((category) => named_collections[category]) - .join(',')); - } - // collections overwrites categories - if (collections !== undefined) unsplash_query.set('collections', collections); - if (topics !== undefined) unsplash_query.set('topics', topics); - if (username !== undefined) unsplash_query.set('username', username); - if (unsplash_query.get('collections') === null) unsplash_query.set('collections', Object.values(named_collections).join(',')); - const data = await getUnsplashImage(unsplash_query, req.query.quality ?? 'normal', ...rest); - return json(data, { headers: { 'Cache-Control': 'no-store' } }); - }) - .get('/images/unsplash/topics', async (req, env) => { - const data = await ( - await fetch(`https://api.unsplash.com/topics?client_id=${env.UNSPLASH_TOKEN}`, { - cf: { cacheTtl: 86400 }, - }) - ).json(); - return json(data, { - // cdn 1w, client 1d, stale 1d - headers: { 'Cache-Control': 'public, s-max-age=604800, max-age=86400, stale-while-revalidate=86400' }, - }); - }) - .get('/quotes/languages', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_quote_languages'); - return data; - }) - .get('/map', async (req, env) => { - const { latitude, longitude } = req.query; - if (!latitude) return error(400, 'latitude is required'); - if (!longitude) return error(400, 'longitude is required'); - const url = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/pin-s+555555(${longitude},${latitude})/${longitude},${latitude},9},0/450x200?access_token=${env.MAPBOX_TOKEN}`; - const res = await fetch(url, { cf: { cacheTtl: 31536000 } }); // 1 year - return new Response(res.body, res); - }) - .get('/news', () => news) - .get('/quotes/random', async (req, env, ctx) => { - const kv_id = 'v2_quote_languages'; - let allowed = await env.cache.get(kv_id, { - cacheTtl: 3600, // cache at this location for an hour - type: 'json', - }); - if (!allowed) { - const { data } = await ctx.$supabase.rpc('get_quote_languages'); - // save for 1 day - ctx.waitUntil(env.cache.put(kv_id, JSON.stringify(data), { expirationTtl: 86400 })); - allowed = data; - } - allowed = allowed.map((row) => row.name); - const language = req.query.language || 'en'; - if (!allowed.includes(language)) return error(400, 'Unsupported language'); - const { data } = await ctx.$supabase.rpc('get_random_quote', { _language: language }).single(); - return json(data, { headers: { 'Cache-Control': 'no-store' } }); - }) - .get('/search/autocomplete', async (req) => { - const search = new URLSearchParams(req.query).toString(); - const url = 'https://ac.ecosia.org/?' + search; - const headers = new Headers(req.headers); - headers.append('x-forwarded-for', req.headers.get('cf-connecting-ip')); - const res = await fetch(new Request(url, { headers })); - return json(await res.json()); - }) - .get('/stats', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .get('/versions', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .get('/weather', withWeatherLanguage, async (req, env, ctx) => { - const { city } = req.query; - if (!city) return error(400, '`city` param is required'); - const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${env.OPENWEATHER_TOKEN}&lang=${ctx.$language}`; - const data = await (await fetch(url)).json(); - if (data.cod === '404') return error(404, 'No data. Try another city?'); - // 10m (too short for cf), stale 5m - return json(data, { headers: { 'Cache-Control': 'private, max-age=600, stale-while-revalidate=300' } }); - }); diff --git a/src/v2/marketplace.js b/src/v2/marketplace.js deleted file mode 100644 index 3fc0471..0000000 --- a/src/v2/marketplace.js +++ /dev/null @@ -1,170 +0,0 @@ -import { error, json } from 'itty-router-extras'; -import paginate from '../pagination.js'; - -/** - * @param {Request} req - */ -function getVersion(req) { - const url = new URL(req.url); - const path = url.pathname; - let version = path.split('/')[1]; - if (!version.startsWith('v')) version = 1; - else version = parseInt(version.slice(1)); - return version; -} - -async function getManifest() { - const manifest = await ( - await fetch('https://marketplace-data.muetab.com/manifest.json?v=2', { cf: { cacheTtl: 3600 } }) - ).json(); - return manifest; -} - -/** - * @param {Request} req - */ -export async function getCollection(req) { - const manifest = await getManifest(); - const collection = manifest.collections[req.params.collection]; - if (collection === undefined) { - return error(404, 'Not Found'); - } - const unresolved_items = collection.items; - collection.items = unresolved_items.map((item) => { - const [type, name] = item.split('/'); - return { - ...manifest[type][name], - type, - }; - }); - - const version = getVersion(req); - if (version === 1) { - collection.name = collection.display_name; - delete collection.display_name; - } - - return json({ data: collection }); -} - -export async function getCollections(req) { - const manifest = await getManifest(); - const collections = Object.values(manifest.collections).map((collection) => { - delete collection.items; - return collection; - }); - return json({ data: paginate(collections, req.query) }); -} - -/** - * @param {Request} req - */ -export async function getCurator(req) { - const manifest = await getManifest(); - const curator = manifest.curators[decodeURIComponent(req.params.curator)]; - if (curator === undefined) { - return error(404, 'Not Found'); - } - const items = curator.map((item) => { - const [type, name] = item.split('/'); - return { - ...manifest[type][name], - type, - }; - }); - return json({ data: { items } }); -} - -export async function getCurators(req) { - const manifest = await getManifest(); - const curators = Object.keys(manifest.curators); - return json({ data: paginate(curators, req.query) }); -} - -export async function getFeatured() { - return json({ - data: await ( - await fetch('https://marketplace-data.muetab.com/featured.json', { cf: { cacheTtl: 3600 } }) - ).json(), - }); -} - -/** - * @param {Request} req - */ -export async function getItem(req) { - const manifest = await getManifest(); - if (!manifest[req.params.category]) { - return error(404, 'Category Not Found'); - } - if (manifest[req.params.category][req.params.item] === undefined) { - return error(404, 'Item Not Found'); - } - let item = await ( - await fetch( - `https://marketplace-data.muetab.com/${req.params.category}/${req.params.item}.json`, - { cf: { cacheTtl: 3600 } }, - ) - ).json(); - - item.in_collections = manifest[req.params.category][req.params.item].in_collections.map( - (name) => { - const collection = manifest.collections[name]; - delete collection.items; - return collection; - }, - ); - - const version = getVersion(req); - if (version === 2) { - item = { - display_name: item.name, - ...item, - name: req.params.item, - }; - } - - return json({ - data: item, - updated: item.updated_at, - }); -} - -/** - * @param {Request} req - */ -export async function getItems(req) { - let data; - const manifest = await getManifest(); - if (req.params.category === 'all') { - data = [ - ...Object.values(manifest.preset_settings).map((item) => { - item.type = 'preset_settings'; - return item; - }), - ...Object.values(manifest.photo_packs).map((item) => { - item.type = 'photo_packs'; - return item; - }), - ...Object.values(manifest.quote_packs).map((item) => { - item.type = 'quote_packs'; - return item; - }), - ]; - } else { - const category = manifest[req.params.category]; - if (!category) { - return error(404, 'Not Found'); - } - data = Object.values(category); - const version = getVersion(req); - if (version === 1) { - data = data.map((item) => { - item.type = req.params.category; - return item; - }); - } - } - data = paginate(data, req.query); - return json({ data }); -} diff --git a/src/v2/pexels.js b/src/v2/pexels.js deleted file mode 100644 index 60c5383..0000000 --- a/src/v2/pexels.js +++ /dev/null @@ -1,24 +0,0 @@ -const sizes = { - original: '', - high: '?w=3840', // eslint-disable-line sort-keys - normal: '?w=1920', - datasaver: '?w=1280', // eslint-disable-line sort-keys -}; - -export const getPexelsImage = async (quality, ...rest) => { - const [env] = rest; - const size = sizes[quality]; - const url = `https://api.pexels.com/v1/collections/${env.PEXELS_COLLECTION}?per_page=80&page=${ - Math.floor(Math.random() * 2) + 1 - }`; - let data = await (await fetch(url, { headers: { Authorization: env.PEXELS_TOKEN } })).json(); - data = data.media[Math.floor(Math.random() * data.media.length) + 1]; - return { - colour: data.average_color, - file: data.src.original + size, - id: data.id, - photo_page: data.url, - photographer: data.photographer, - photographer_page: data.photographer_url, - }; -}; diff --git a/src/v2/sponsors.js b/src/v2/sponsors.js deleted file mode 100644 index 7cb8780..0000000 --- a/src/v2/sponsors.js +++ /dev/null @@ -1,16 +0,0 @@ -import cheerio from 'cheerio'; - -export const getSponsors = async () => { - const data = await ( - await fetch(`https://github.com/sponsors/${env.SPONSORS_NAME}/sponsors_partial?page=1`) - ).text(); - const $ = cheerio.load(data); - const sponsors = []; - $('.d-inline-block').each((i, el) => { - sponsors.push({ - img: $(el).attr('href').replace('/', ''), - name: $(el).find('img').attr('alt'), - }); - }); - return { sponsors }; -}; diff --git a/src/v2/unsplash.js b/src/v2/unsplash.js deleted file mode 100644 index 737f20c..0000000 --- a/src/v2/unsplash.js +++ /dev/null @@ -1,37 +0,0 @@ -const sizes = { - original: '', - high: '&w=3840', // eslint-disable-line sort-keys - normal: '&w=1920', - datasaver: '&w=1280', // eslint-disable-line sort-keys -}; - -export const getUnsplashImage = async (query, quality, ...rest) => { - const [env, ctx] = rest; - query.set('client_id', env.UNSPLASH_TOKEN); - const ref = `?utm_source=${env.UNSPLASH_REFERRAL}&utm_medium=referral`; - const size = sizes[quality]; - const data = await ( - await fetch(`https://api.unsplash.com/photos/random?${query.toString()}`) - ).json(); - ctx.waitUntil(fetch(`${data.links.download_location}&client_id=${env.UNSPLASH_TOKEN}`)); // api requirement - return { - blur_hash: data.blur_hash, - camera: data.exif.model ?? null, - category: null, - colour: data.color, - description: data.description ?? null, - downloads: data.downloads, - file: data.urls.raw + '&q=80&auto=format' + size, - id: data.id, - likes: data.likes, - location: { - latitude: data.location?.position?.latitude ?? null, - longitude: data.location?.position?.longitude ?? null, - name: data.location?.name ?? null, - }, - photo_page: data.links.html + ref, - photographer: data.user.name, - photographer_page: data.user.links.html + ref, // api requirement - views: data.views, - }; -}; diff --git a/src/v2/weather.js b/src/v2/weather.js deleted file mode 100644 index e7d2286..0000000 --- a/src/v2/weather.js +++ /dev/null @@ -1,24 +0,0 @@ -import { error } from 'itty-router-extras'; - -export const withWeatherLanguage = (req, env, ctx) => { - const allowed = ['en', 'de', 'es', 'fr', 'nl', 'no', 'ru', 'zh_cn', 'id', 'tr', 'pt', 'bn']; - const map = new Map([ - ['de_DE', 'de'], - ['en_GB', 'en'], - ['en_US', 'en'], - ['zh_CN', 'zh_cn'], - ['pt_BR', 'pt'], - ['es_419', 'es'], - ['tr_TR', 'tr'], - ['id_ID', 'id'], - ]); - let { language } = req.query; - if (!language) { - language = 'en'; - } else if (map.has(language)) { - language = map.get(language); - } else if (!allowed.includes(language)) { - return error(400, 'Unsupported language'); - } - ctx.$language = language; -}; diff --git a/supabase.sql b/supabase.sql deleted file mode 100644 index 5595b36..0000000 --- a/supabase.sql +++ /dev/null @@ -1,108 +0,0 @@ -create or replace index on "public"."images" using btree (category); - -create or replace index on "public"."quotes" using btree (language); - -create or replace function get_image_categories() -returns table ( - name text, - count bigint -) -language plpgsql -as $$ -begin - return query - select - category as name, - count(category) - from images - group by category - order by count(category) desc; -end;$$; - - -create or replace function get_image_photographers() -returns table ( - name text, - count bigint -) -language plpgsql -as $$ -begin - return query - select - photographer as name, - count(photographer) - from images - group by photographer - order by count(photographer) desc; -end;$$; - - -create or replace function get_quote_languages() -returns table ( - name text, - count bigint -) -language plpgsql -as $$ -begin - return query - select - language as name, - count(language) - from quotes - group by language - order by count(language) desc; -end;$$; - - -create or replace function get_random_image( - _category text default 'landscapes', - _exclude text default '' -) -returns setof images -language plpgsql -as $$ -begin - return query - select * - from images - where - category = _category - and - cast(pun as text) not in (select * from unnest(string_to_array(_exclude, ','))) - order by random() - limit 1; -end;$$; - - -create or replace function get_random_old_quote( - _language text default 'English' -) -returns setof old_quotes -language plpgsql -as $$ -begin - return query - select * - from old_quotes - where language = _language - order by random() - limit 1; -end;$$; - - -create or replace function get_random_quote( - _language text default 'en' -) -returns setof quotes -language plpgsql -as $$ -begin - return query - select * - from quotes - where language = _language - order by random() - limit 1; -end;$$; diff --git a/wrangler.toml b/wrangler.toml deleted file mode 100644 index 6d770d1..0000000 --- a/wrangler.toml +++ /dev/null @@ -1,36 +0,0 @@ -account_id = "950e96e5beb222c4eeb9962512a6d6fc" -name = "api" -main = "src/index.js" -compatibility_date = "2024-04-05" -compatibility_flags = [ "nodejs_compat" ] -upload_source_maps = true -workers_dev = true -routes = [ - { pattern = "api.muetab.com", custom_domain = true, zone_name = "muetab.com" }, - { pattern = "marketplace.muetab.com", custom_domain = true, zone_name = "muetab.com" } -] -kv_namespaces = [ - { binding = "cache", id = "08c0008096b7486a9532c5be43e48a7f" } -] - -[[services]] - binding = "WEBSTORES" - service = "webstores-api" - -# [[d1_databases]] -# binding = "DB" -# database_name = "mue-content" -# database_id = "5b6376e6-1d2a-4f3b-b463-0d6a1b387be1" - -[vars] - UNSPLASH_REFERRAL = "mue" - PEXELS_COLLECTION = "iwf78dt" - SPONSORS_NAME = "mue" - -# SECRETS: -# SUPABASE_URL -# SUPABASE_TOKEN -# UNSPLASH_TOKEN -# PEXELS_TOKEN -# MAPBOX_TOKEN -# OPENWEATHER_TOKEN