mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-06 06:36:25 +00:00
Compare commits
285 Commits
feat/runti
...
12fe7c157f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12fe7c157f | ||
| c29ad99ca0 | |||
|
|
a99f29074c | ||
|
|
f449a0878a | ||
|
|
097985ab6d | ||
| 11c300ecf7 | |||
| 1e975ce238 | |||
| b39fa4262d | |||
| d7699389bc | |||
|
|
1036e9f3f3 | ||
|
|
b4749f3ed3 | ||
|
|
a20c61206f | ||
|
|
0644a43cb1 | ||
| af4742ae0b | |||
|
|
e401ca98c0 | ||
|
|
a756c875fd | ||
|
|
2ef26e5d02 | ||
|
|
e7d9002156 | ||
|
|
28d2e193aa | ||
| ce5bee11c0 | |||
| 60d59d7f7b | |||
| 464d720ef9 | |||
| 8fc279d2ed | |||
| a45e992339 | |||
| 5f8ddd435a | |||
| d822463fe0 | |||
|
|
3a0cbf786d | ||
|
|
dfb4777a5d | ||
|
|
eea32c47e9 | ||
|
|
6bcd03b18e | ||
|
|
87a3df6897 | ||
|
|
7f7a16e9b5 | ||
|
|
b95dd9056b | ||
|
|
5044f941b1 | ||
| c56f9ea791 | |||
| eb56dd70d6 | |||
| a4f5ef33ff | |||
| 20ab1dae6c | |||
| 7ebc0fe504 | |||
| 019aceb8d9 | |||
| f59cb5d671 | |||
| d4deafe1dc | |||
| 7b2f1c7a82 | |||
| c5fa3ecb01 | |||
| 3602905e86 | |||
| 1f7844b8a5 | |||
|
|
3b76fb2647 | ||
|
|
9a00d5036f | ||
|
|
7c315602cd | ||
|
|
19e0e402da | ||
|
|
ef38468178 | ||
| 2cbbb450c2 | |||
| 9f466ff702 | |||
| 05f7fabb3c | |||
| 5bc6a06b91 | |||
| f7e801e574 | |||
|
|
c663189df1 | ||
| 37ec32b52d | |||
| 188ce3f67d | |||
| 18b2ae2c5f | |||
| a115c83cba | |||
| 27d25f4829 | |||
|
|
d1d6fa9556 | ||
|
|
7878673d8d | ||
| 34761b43ae | |||
|
|
536b03b1ef | ||
|
|
6f37c128ef | ||
|
|
8e3a582f75 | ||
|
|
b5720dca06 | ||
|
|
df26dbca63 | ||
|
|
851ae9eb9d | ||
|
|
8aaf786400 | ||
| 3e40842c84 | |||
| 8ab4146241 | |||
| 0fa25eaeca | |||
| 2194831d86 | |||
| 7124a3d3c6 | |||
| 55a22f1c9e | |||
| 6d58164a6d | |||
| fea9c16515 | |||
| 01883d08cc | |||
|
|
03792487c3 | ||
|
|
87a0fa39f7 | ||
|
|
64dae6ddce | ||
|
|
efec489c96 | ||
|
|
5837b9875d | ||
| 1e1a6a1159 | |||
| 5e63b57440 | |||
|
|
befc0fc84f | ||
|
|
9bbdb3d7f0 | ||
|
|
58690eb428 | ||
|
|
7d47b7642c | ||
|
|
2f3682c226 | ||
|
|
d952444919 | ||
|
|
86cb391425 | ||
|
|
ed6623293b | ||
|
|
7a756dd67c | ||
|
|
e71640a636 | ||
|
|
f4b1ab5fa0 | ||
|
|
90475e47b1 | ||
|
|
c32b58e974 | ||
|
|
211f75f71a | ||
|
|
f0b9f3afdc | ||
|
|
6bc041723e | ||
|
|
82ea4fbe0b | ||
|
|
1749dc814b | ||
|
|
dfc886127a | ||
|
|
4943c94182 | ||
|
|
238702f81c | ||
|
|
c60abb26f9 | ||
|
|
2fb393bb45 | ||
|
|
6bab124331 | ||
|
|
9c57e01426 | ||
|
|
66dedaee29 | ||
|
|
eb31c0d8e6 | ||
|
|
cd65632527 | ||
|
|
a563d8f8ba | ||
|
|
60082ee799 | ||
|
|
6065b73d13 | ||
|
|
32a9dfc11c | ||
|
|
57c135c86b | ||
|
|
5064111a93 | ||
|
|
de718b6a46 | ||
|
|
d7748eb83e | ||
|
|
d730b5f3f4 | ||
|
|
5d17f8f0f9 | ||
|
|
864ee3efa2 | ||
|
|
a14a0145a9 | ||
|
|
a86c361825 | ||
|
|
e17e969bfe | ||
|
|
4708186f5c | ||
|
|
6238c8d9a0 | ||
|
|
7a34bbedae | ||
|
|
e59477eddd | ||
|
|
7368af7266 | ||
|
|
9c03f99524 | ||
|
|
54d4965a9a | ||
|
|
dc7d6919da | ||
|
|
aa180bfcea | ||
|
|
5ab28622d9 | ||
|
|
7f97ea6e90 | ||
|
|
869b0206b8 | ||
|
|
159a4cc77a | ||
|
|
571590a40d | ||
|
|
0943401d03 | ||
|
|
ababb67b1a | ||
|
|
5bf4d70623 | ||
|
|
c495589927 | ||
|
|
5e20257202 | ||
|
|
e8154c31ce | ||
|
|
b1d8d00a9b | ||
|
|
c5c0de5493 | ||
|
|
cfafe12c82 | ||
|
|
594e0233ca | ||
|
|
63c5b40123 | ||
| 1caff13adc | |||
| b4c85f3f28 | |||
| a95bbcb6eb | |||
| 61b38d5b03 | |||
| 563ae85db1 | |||
| 8f0fb42b47 | |||
| 40c13e7ddf | |||
| 0a862c3782 | |||
| b1723c2f2c | |||
|
|
84fcbbbb42 | ||
|
|
93b5b50ba1 | ||
| 4f9d340ef4 | |||
| 5142e2bc25 | |||
| 8f7f388403 | |||
| 5a37327e63 | |||
| d42062679a | |||
| 6bb905b388 | |||
| 39cfd501ac | |||
| 079cc6b4f9 | |||
| 951ae955ed | |||
| 61708857af | |||
| d3c69876d4 | |||
| ebaf6d2177 | |||
| 9bc30ab62d | |||
|
|
f71a65d134 | ||
|
|
ca0722b55c | ||
|
|
22cf24fd8c | ||
|
|
19e3619a42 | ||
| 1db4dea56f | |||
| 03bb51661a | |||
| 01177c2489 | |||
| a86cd969a3 | |||
| bc6c93c9c7 | |||
| 572ddc69ad | |||
| c1b243df9c | |||
| 31500dc3c5 | |||
| 509e7c08cd | |||
| 165d9e8f31 | |||
| 04171af3e3 | |||
| f1ddc7e7b9 | |||
| 5827cc32e8 | |||
| 4dc34641ec | |||
|
|
14c8f25499 | ||
|
|
d98ac3b452 | ||
| 3e7b27342c | |||
| dd9e611bef | |||
|
|
0905e4424e | ||
|
|
64c43a7833 | ||
| 648a03e3ea | |||
|
|
eb6887d189 | ||
|
|
c6133d85ff | ||
|
|
8a1b90f035 | ||
|
|
8348e185fc | ||
|
|
1d78b26a37 | ||
| a8b2e575d5 | |||
| c132afe163 | |||
| ebad8f32a4 | |||
| fb74ed30f8 | |||
| a0be0555e0 | |||
| 002b9e4b35 | |||
| 996ad52159 | |||
| 3b8c21c20f | |||
| b3b58f7a1e | |||
| 7739908a19 | |||
| a3f29c73ec | |||
| 5fdc96db64 | |||
| 9206a30182 | |||
| 1d81981c3f | |||
| a15f28e541 | |||
| 23832929e9 | |||
| 3590963206 | |||
| 5ca1ae938f | |||
| dfdeca35f3 | |||
| 70ff2285d5 | |||
| c5f237771c | |||
| 8fea8b1fe7 | |||
| 816ee8de14 | |||
| a7d5f94dfb | |||
| b7c6ba1e85 | |||
| e348464261 | |||
| 5031cc7163 | |||
| ebfb486363 | |||
| 6cb1ae5fa1 | |||
| ad41c09055 | |||
| 5b320974df | |||
| e01c67b1ef | |||
| da5823deb2 | |||
| 7085a68733 | |||
| 84a855602e | |||
| 57779f02b1 | |||
| becc550add | |||
| 9343bb524c | |||
| fc9695a2dc | |||
| a310ceaed5 | |||
| 4d8806fc7f | |||
| d70b45d1fd | |||
| 5eb067639b | |||
| c364d3a67e | |||
| 666ad9279f | |||
| 02c77d2f32 | |||
| ebbed77650 | |||
| 578dc4bbc9 | |||
| e3ae961b68 | |||
| c33ba01e54 | |||
| 787bfc1151 | |||
| f12718d67f | |||
| df3e0d1ed7 | |||
| 8ffed25580 | |||
| a0739e57f2 | |||
|
|
da79d235be | ||
|
|
ae20682c92 | ||
|
|
99d4a21c12 | ||
|
|
17040e9775 | ||
|
|
f92f93f960 | ||
|
|
1eb832f00d | ||
|
|
b14ada8f48 | ||
|
|
f5f7a187c4 | ||
|
|
938f7ca047 | ||
|
|
b0c1df2827 | ||
|
|
be0b5d1523 | ||
|
|
fbee956876 | ||
|
|
db1c1a5e92 | ||
|
|
b4f9552048 | ||
|
|
1a5b93e5aa | ||
|
|
216a58af24 | ||
|
|
eff7a32b09 | ||
|
|
bd041c1a4e | ||
|
|
48876494f7 | ||
|
|
e4d92ff364 | ||
|
|
d35ab8c894 |
66
.env.example
66
.env.example
@@ -10,6 +10,8 @@ LIBRARY_ROOT=./video
|
||||
# You should set this to a path where kyoo can write large amount of data, this is used as a cache by the transcoder.
|
||||
# It will automatically be cleaned up on kyoo's startup/shutdown/runtime.
|
||||
CACHE_ROOT=/tmp/kyoo_cache
|
||||
# Where to store downloaded images of the shows
|
||||
IMAGES_PATH="./images";
|
||||
# A pattern (regex) to ignore files.
|
||||
LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*"
|
||||
|
||||
@@ -21,8 +23,8 @@ GOCODER_PRESET=fast
|
||||
|
||||
|
||||
# Keep those empty to use kyoo's default api key. You can also specify a custom API key if you want.
|
||||
# go to https://www.themoviedb.org/settings/api and copy the api key (not the read access token, the api key)
|
||||
THEMOVIEDB_APIKEY=
|
||||
# go to https://www.themoviedb.org/settings/api and copy the read access token (not the api key)
|
||||
THEMOVIEDB_API_ACCESS_TOKEN=""
|
||||
# go to https://thetvdb.com/api-information/signup and copy the api key
|
||||
TVDB_APIKEY=
|
||||
# you can also input your subscriber's pin to support TVDB
|
||||
@@ -32,40 +34,48 @@ TVDB_PIN=
|
||||
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
||||
PUBLIC_URL=http://localhost:8901
|
||||
|
||||
# Use a builtin oidc service (google, discord, trakt, or simkl):
|
||||
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
|
||||
OIDC_DISCORD_CLIENTID=
|
||||
OIDC_DISCORD_SECRET=
|
||||
# Or add your custom one:
|
||||
OIDC_SERVICE_NAME=YourPrettyName
|
||||
OIDC_SERVICE_LOGO=https://url-of-your-logo.com
|
||||
OIDC_SERVICE_CLIENTID=
|
||||
OIDC_SERVICE_SECRET=
|
||||
OIDC_SERVICE_AUTHORIZATION=https://url-of-the-authorization-endpoint-of-the-oidc-service.com/auth
|
||||
OIDC_SERVICE_TOKEN=https://url-of-the-token-endpoint-of-the-oidc-service.com/token
|
||||
OIDC_SERVICE_PROFILE=https://url-of-the-profile-endpoint-of-the-oidc-service.com/userinfo
|
||||
OIDC_SERVICE_SCOPE="the list of scopes space separeted like email identity"
|
||||
# Token authentication method as seen in https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
||||
# Supported values: ClientSecretBasic (default) or ClientSecretPost
|
||||
# If in doubt, leave this empty.
|
||||
OIDC_SERVICE_AUTHMETHOD=ClientSecretBasic
|
||||
# on the previous list, service is the internal name of your service, you can add as many as you want.
|
||||
# Default permissions of new users. They are able to browse & play videos.
|
||||
# Set `verified` to true if you don't wanna manually verify users.
|
||||
EXTRA_CLAIMS='{"permissions": ["core.read", "core.play"], "verified": false}'
|
||||
# This is the permissions of the first user (aka the first user is admin)
|
||||
FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "users.delete", "apikeys.read", "apikeys.write", "core.read", "core.write", "core.play", "scanner.trigger"], "verified": true}'
|
||||
|
||||
# Guest (meaning unlogged in users) can be:
|
||||
# unauthorized (they need to connect before doing anything)
|
||||
# GUEST_CLAIMS=""
|
||||
# able to browse & see what you have but not able to play
|
||||
GUEST_CLAIMS='{"permissions": ["core.read"], "verified": true}'
|
||||
# or have browse & play permissions
|
||||
GUEST_CLAIMS='{"permissions": ["core.read", "core.play"], "verified": true}'
|
||||
|
||||
# DO NOT change this.
|
||||
PROTECTED_CLAIMS="permissions,verified"
|
||||
|
||||
|
||||
# Following options are optional and only useful for debugging.
|
||||
# You can create apikeys at runtime via POST /keys but you can also have some defined in the env.
|
||||
# Replace $YOURNAME with the name of the key you want (only alpha are valid)
|
||||
# The value will be the apikey (max 128 bytes)
|
||||
KEIBI_APIKEY_SCANNER=EJqUB8robwKwLNt37SuHqdcsNGrtwpfYxeExfiAbokpxZVd4WctWr7gnSZ
|
||||
KEIBI_APIKEY_SCANNER_CLAIMS='{"permissions": ["core.read", "core.write"]}'
|
||||
|
||||
# To debug the front end, you can set the following to an external backend
|
||||
KYOO_URL=
|
||||
|
||||
# Database things
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
|
||||
|
||||
# It is recommended to use the below PG environment variables when possible.
|
||||
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
|
||||
# The behavior of the below variables match what is documented here:
|
||||
# https://www.postgresql.org/docs/current/libpq-envars.html
|
||||
PGUSER=kyoo
|
||||
PGPASSWORD=password
|
||||
PGDATABASE=kyoo
|
||||
PGHOST=postgres
|
||||
PGPORT=5432
|
||||
|
||||
# v5 stuff, does absolutely nothing on master (aka: you can delete this)
|
||||
EXTRA_CLAIMS='{"permissions": ["core.read"], "verified": false}'
|
||||
FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write", "scanner.trigger"], "verified": true}'
|
||||
GUEST_CLAIMS='{"permissions": ["core.read"]}'
|
||||
PROTECTED_CLAIMS="permissions,verified"
|
||||
# PGOPTIONS=-c search_path=kyoo,public
|
||||
# PGPASSFILE=/my/password # Takes precedence over PGPASSWORD. New line characters are not trimmed.
|
||||
# PGSSLMODE=verify-full
|
||||
# PGSSLROOTCERT=/my/serving.crt
|
||||
# PGSSLCERT=/my/client.crt
|
||||
# PGSSLKEY=/my/client.key=password
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
7e6e56a366babe17e7891a5897180efbf93c00c5
|
||||
a5638203a6ecb9f372a5a61e1c8fd443bf3a17fe
|
||||
18e301f26acd7f2e97eac26c5f48377fa13956f5
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.Designer.cs linguist-generated=true
|
||||
6
.github/workflows/api-test.yml
vendored
6
.github/workflows/api-test.yml
vendored
@@ -15,17 +15,18 @@ jobs:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_USER: kyoo
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: kyoo_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -37,3 +38,4 @@ jobs:
|
||||
run: bun test
|
||||
env:
|
||||
PGHOST: localhost
|
||||
IMAGES_PATH: ./images
|
||||
|
||||
4
.github/workflows/auth-hurl.yml
vendored
4
.github/workflows/auth-hurl.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_USER: kyoo
|
||||
POSTGRES_PASSWORD: password
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: gacts/install-hurl@v1
|
||||
|
||||
|
||||
26
.github/workflows/coding-style.yml
vendored
26
.github/workflows/coding-style.yml
vendored
@@ -2,20 +2,6 @@ name: Coding Style
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
back:
|
||||
name: "Lint Back"
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./back
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Check coding style
|
||||
run: |
|
||||
dotnet tool restore
|
||||
dotnet csharpier . --check
|
||||
|
||||
api:
|
||||
name: "Lint api"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -23,7 +9,7 @@ jobs:
|
||||
run:
|
||||
working-directory: ./api
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Biome
|
||||
uses: biomejs/setup-biome@v2
|
||||
@@ -40,7 +26,7 @@ jobs:
|
||||
run:
|
||||
working-directory: ./front
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Biome
|
||||
uses: biomejs/setup-biome@v2
|
||||
@@ -51,10 +37,10 @@ jobs:
|
||||
run: biome ci .
|
||||
|
||||
scanner:
|
||||
name: "Lint scanner/autosync"
|
||||
name: "Lint scanner"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
@@ -67,7 +53,7 @@ jobs:
|
||||
run:
|
||||
working-directory: ./transcoder
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Run go fmt
|
||||
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
|
||||
@@ -79,7 +65,7 @@ jobs:
|
||||
run:
|
||||
working-directory: ./auth
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Run go fmt
|
||||
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
|
||||
|
||||
19
.github/workflows/docker.yml
vendored
19
.github/workflows/docker.yml
vendored
@@ -19,16 +19,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- context: ./back
|
||||
dockerfile: Dockerfile
|
||||
label: back
|
||||
image: ${{ github.repository_owner }}/kyoo_back
|
||||
|
||||
- context: ./back
|
||||
dockerfile: Dockerfile.migrations
|
||||
label: migrations
|
||||
image: ${{ github.repository_owner }}/kyoo_migrations
|
||||
|
||||
- context: ./api
|
||||
dockerfile: Dockerfile
|
||||
label: api
|
||||
@@ -44,11 +34,6 @@ jobs:
|
||||
label: scanner
|
||||
image: ${{ github.repository_owner }}/kyoo_scanner
|
||||
|
||||
- context: ./autosync
|
||||
dockerfile: Dockerfile
|
||||
label: autosync
|
||||
image: ${{ github.repository_owner }}/kyoo_autosync
|
||||
|
||||
- context: ./transcoder
|
||||
dockerfile: Dockerfile
|
||||
label: transcoder
|
||||
@@ -57,12 +42,12 @@ jobs:
|
||||
- context: ./auth
|
||||
dockerfile: Dockerfile
|
||||
label: auth
|
||||
image: ${{ github.repository_owner }}/keibi
|
||||
image: ${{ github.repository_owner }}/kyoo_auth
|
||||
env:
|
||||
DOCKERHUB_ENABLED: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_PASSWORD && 'true' || 'false' }}
|
||||
name: Build ${{matrix.label}}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
|
||||
64
.github/workflows/helm-release-chart.yml
vendored
64
.github/workflows/helm-release-chart.yml
vendored
@@ -2,41 +2,53 @@ name: Release Helm Chart
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: 'Release channel (master, edge, or leave blank for tag-based)'
|
||||
required: false
|
||||
default: 'master'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update Helm Dependencies
|
||||
run: |
|
||||
helm dependency update ./chart
|
||||
- name: Update Helm Dependencies
|
||||
run: helm dependency update ./chart
|
||||
|
||||
- name: Package Helm Chart
|
||||
run: |
|
||||
export tag=$(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
|
||||
helm package ./chart --version $tag --app-version $tag
|
||||
|
||||
- name: Build Helm-safe repo name
|
||||
run: |
|
||||
REPO_NAME="$(echo "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/helm-charts" | tr '[:upper:]' '[:lower:]')"
|
||||
echo "REPO_NAME=${REPO_NAME}" >> "${GITHUB_ENV}"
|
||||
- name: Determine Chart Version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG="${{ github.event.inputs.channel }}"
|
||||
else
|
||||
TAG=$(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
|
||||
fi
|
||||
echo "TAG=$TAG" >> "${GITHUB_ENV}"
|
||||
echo "Using chart version: $TAG"
|
||||
|
||||
- name: Push Helm Chart to GHCR
|
||||
run: |
|
||||
helm push kyoo-*.tgz "${REPO_NAME}"
|
||||
- name: Package Helm Chart
|
||||
run: helm package ./chart --version $TAG --app-version $TAG
|
||||
|
||||
- name: Build Helm-safe repo name
|
||||
run: |
|
||||
REPO_NAME="$(echo "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/helm-charts" | tr '[:upper:]' '[:lower:]')"
|
||||
echo "REPO_NAME=${REPO_NAME}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Push Helm Chart to GHCR
|
||||
run: helm push kyoo-*.tgz "${REPO_NAME}"
|
||||
|
||||
2
.github/workflows/helm-test-chart.yml
vendored
2
.github/workflows/helm-test-chart.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
10
.github/workflows/native-build.yml
vendored
10
.github/workflows/native-build.yml
vendored
@@ -2,7 +2,7 @@ name: Native build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
update:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
working-directory: ./front
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# This is required because GHA doesn't support secrets in the `if` condition
|
||||
- name: Check if Expo build is enabled
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
echo "Expo build is disabled for forks. To enable it, add an EXPO_TOKEN secret to this repository. See https://docs.expo.dev/eas-update/github-actions/ for more information."
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
if: env.IS_EXPO_ENABLED == 'true'
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
cache: yarn
|
||||
cache-dependency-path: front/yarn.lock
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
if: env.IS_EXPO_ENABLED == 'true'
|
||||
run: wget -O kyoo.apk ${{ steps.url.outputs.assetUrl }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v5
|
||||
if: env.IS_EXPO_ENABLED == 'true'
|
||||
with:
|
||||
name: kyoo.apk
|
||||
|
||||
6
.github/workflows/native-update.yml
vendored
6
.github/workflows/native-update.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
working-directory: ./front
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# This is required because GHA doesn't support secrets in the `if` condition
|
||||
- name: Check if Expo build is enabled
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
echo "Expo build is disabled for forks. To enable it, add an EXPO_TOKEN secret to this repository. See https://docs.expo.dev/eas-update/github-actions/ for more information."
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
if: env.IS_EXPO_ENABLED == 'true'
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
cache: yarn
|
||||
cache-dependency-path: front/yarn.lock
|
||||
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
update:
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set correct versions
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/video
|
||||
.devspace/
|
||||
.env
|
||||
.venv
|
||||
.idea
|
||||
@@ -6,6 +7,4 @@
|
||||
log.html
|
||||
output.xml
|
||||
report.html
|
||||
chart/charts
|
||||
chart/Chart.lock
|
||||
tmp
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Authors
|
||||
Ordered by the date of the first commit.
|
||||
|
||||
* Zoe Roux ([@zoriya](http://github.com/zoriya))
|
||||
325
DIAGRAMS.md
325
DIAGRAMS.md
@@ -5,27 +5,26 @@ These diagrams are created with Mermaid and rendered locally. For the best expe
|
||||
Kyoo is a monorepo that consists of several projects each in their own directory. Diagram below shows an outline of kyoo, projects, and artifacts.
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
block:proj1:1
|
||||
proj_name["Kyoo"]:1
|
||||
end
|
||||
block:proj2:1
|
||||
dir_1["autosync/"]
|
||||
dir_2["back/"]
|
||||
dir_1["api/"]
|
||||
dir_2["auth/"]
|
||||
dir_3["front/"]
|
||||
dir_4["transcoder/"]
|
||||
dir_5["scanner/"]
|
||||
end
|
||||
block:proj3:1
|
||||
%% columns auto (default)
|
||||
block:autosync_b:1
|
||||
autosync_i1("kyoo_autosync")
|
||||
block:api_b:1
|
||||
autosync_i1("kyoo_api")
|
||||
end
|
||||
block:back_b:1
|
||||
block:auth_b:1
|
||||
columns 1
|
||||
back_i1("kyoo_back")
|
||||
back_i2("kyoo_migrations")
|
||||
back_i1("kyoo_auth")
|
||||
end
|
||||
block:front_b:1
|
||||
front_i1("kyoo_front")
|
||||
@@ -36,7 +35,6 @@ block-beta
|
||||
block:scanner_b:1
|
||||
columns 1
|
||||
scanner_i1("kyoo_scanner")
|
||||
scanner_i2("kyoo_scanner*")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,19 +49,17 @@ block-beta
|
||||
style dir_4 fill:#438dd5,stroke-width:0px
|
||||
style dir_5 fill:#438dd5,stroke-width:0px
|
||||
|
||||
style autosync_b fill:#438dd5,stroke-width:0px
|
||||
style back_b fill:#438dd5,stroke-width:0px
|
||||
style api_b fill:#438dd5,stroke-width:0px
|
||||
style auth_b fill:#438dd5,stroke-width:0px
|
||||
style front_b fill:#438dd5,stroke-width:0px
|
||||
style transcoder_b fill:#438dd5,stroke-width:0px
|
||||
style scanner_b fill:#438dd5,stroke-width:0px
|
||||
|
||||
style autosync_i1 fill:#85bbf0,stroke-width:0px
|
||||
style back_i1 fill:#85bbf0,stroke-width:0px
|
||||
style back_i2 fill:#85bbf0,stroke-width:0px
|
||||
style front_i1 fill:#85bbf0,stroke-width:0px
|
||||
style transcoder_i1 fill:#85bbf0,stroke-width:0px
|
||||
style scanner_i1 fill:#85bbf0,stroke-width:0px
|
||||
style scanner_i2 fill:#85bbf0,stroke-width:0px
|
||||
```
|
||||
|
||||
# C4 Diagrams
|
||||
@@ -89,212 +85,165 @@ C4Context
|
||||
```
|
||||
|
||||
## Container
|
||||
Messaging is middleware. EnterpriseMessageBus is for any messaging handled between different projects.
|
||||
Kyoo leverages the [API Gateway](https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway) approach to microservices and [offloads](https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-offloading) authentication at the gateway.
|
||||
|
||||
```mermaid
|
||||
C4Container
|
||||
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="3")
|
||||
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
|
||||
|
||||
title Container diagram for Kyoo System
|
||||
|
||||
Person(user, "User")
|
||||
System_Boundary(internal, "Kyoo") {
|
||||
Container(frontend, "front/")
|
||||
Container(backend, "back/")
|
||||
ContainerQueue(emb, "emb", "", "EnterpriseMessageBus")
|
||||
Container(transcoder, "transcoder/")
|
||||
Container(scanner, "scanner/")
|
||||
Container(autosync, "autosync/")
|
||||
}
|
||||
System_Boundary(external, "") {
|
||||
System_Ext(content, "ContentDatabase", "")
|
||||
}
|
||||
System_Boundary(external2, "") {
|
||||
System_Ext(tracker, "ActivityTracker", "")
|
||||
}
|
||||
System_Boundary(external3, "") {
|
||||
System_Ext(media, "MediaLibrary", "")
|
||||
}
|
||||
Container(apigateway, "API Gateway")
|
||||
Container(auth, "auth")
|
||||
Container(transcoder, "transcoder")
|
||||
Container(scanner, "scanner")
|
||||
Container(frontend, "front")
|
||||
System_Ext(media, "MediaLibrary", "")
|
||||
System_Ext(content, "ContentDatabase", "")
|
||||
Container(api, "api")
|
||||
System_Ext(tracker, "ActivityTracker", "")
|
||||
|
||||
Rel(user, frontend, "")
|
||||
Rel(user, backend, "")
|
||||
Rel(frontend, backend, "")
|
||||
Rel(backend, emb, "")
|
||||
Rel(backend, transcoder, "")
|
||||
Rel_Back(autosync, emb, "")
|
||||
Rel(autosync, tracker, "")
|
||||
Rel_Back(scanner, emb, "")
|
||||
Rel(scanner, backend, "")
|
||||
|
||||
Rel(user, apigateway, "")
|
||||
Rel(apigateway, frontend, "")
|
||||
Rel(apigateway, scanner, "")
|
||||
Rel(apigateway, transcoder, "")
|
||||
Rel(apigateway, api, "")
|
||||
Rel(apigateway, auth, "")
|
||||
Rel(frontend, api, "")
|
||||
Rel(api, tracker, "")
|
||||
Rel(api, content, "")
|
||||
Rel(scanner, api, "")
|
||||
Rel(scanner, media, "")
|
||||
Rel(scanner, content, "")
|
||||
Rel(transcoder, media, "")
|
||||
```
|
||||
|
||||
|
||||
## Component
|
||||
### Autosync
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="2")
|
||||
|
||||
title Component Diagram for Autosync
|
||||
|
||||
Container_Boundary(autosync, "autosync") {
|
||||
Component(autosync_c1, "kyoo_autosync", "python, python3.12", "")
|
||||
}
|
||||
Container_Boundary(emb, "emb") {
|
||||
ComponentQueue(emb_q1, "autosync", "RabbitMQ, Queue", "")
|
||||
ComponentQueue(emb_e1, "events.watched", "RabbitMQ, Exchange", "")
|
||||
|
||||
}
|
||||
Container_Boundary(tracker, "ActivityTracker") {
|
||||
Component_Ext(tracker_c1, "TrackerProvider", "API", "simkl")
|
||||
}
|
||||
Container_Boundary(backend, "back") {
|
||||
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
|
||||
}
|
||||
#### Auth
|
||||
Auth microservice is implicitly used by each other microservice for both end user authentication and microservice to microservice communications. Auth microservice will not be directly represented in the other component diagrams. Instead in their relationsihp will specify "auth via middleware".
|
||||
|
||||
Rel(emb_e1, emb_q1, "bound")
|
||||
Rel_Back(autosync_c1, emb_q1, "consumes")
|
||||
Rel(backend_c2, emb_e1, "produces")
|
||||
Rel(autosync_c1, tracker_c1, "updates")
|
||||
```
|
||||
|
||||
### Back
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="2")
|
||||
|
||||
title Component Diagram for Back
|
||||
title Auth Component Diagram
|
||||
|
||||
Person(user, "User")
|
||||
|
||||
Container_Boundary(frontend, "front") {
|
||||
Component(frontend_c1, "kyoo_front", "typescript, node.js", "Static Content")
|
||||
Container_Boundary(auth, "auth") {
|
||||
Component(auth_c1, "kyoo_auth", "Go", "")
|
||||
ComponentDb(auth_db1, "kelbi", "Postgres", "")
|
||||
}
|
||||
Container_Boundary(backend, "back") {
|
||||
ComponentDb(backend_db2, "search", "Meilisearch", "search resource")
|
||||
Component(backend_c3, "BackendMetadata", "Volume", "Persistent. Distributed Metadata")
|
||||
ComponentDb(backend_db1, "backend", "Postgres", "user data and session state")
|
||||
Component(backend_c1, "kyoo_migrations", "C#, .NET 8.0", "Postgres Migration")
|
||||
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
|
||||
}
|
||||
|
||||
Container_Boundary(emb, "emb") {
|
||||
ComponentQueue(emb_e1, "events.watched", "RabbitMQ, Exchange", "")
|
||||
ComponentQueue(emb_q1, "autosync", "RabbitMQ, Queue", "")
|
||||
ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
|
||||
ComponentQueue(emb_e2, "events.resource", "RabbitMQ, Exchange", "unused")
|
||||
}
|
||||
|
||||
Container_Boundary(scanner, "scanner") {
|
||||
Component(scanner_c2, "kyoo_scanner", "python, python3.12", "matcher")
|
||||
Component(scanner_c1, "kyoo_scanner", "python, python3.12", "scanner")
|
||||
}
|
||||
|
||||
Container_Boundary(autosync, "autosync") {
|
||||
Component(autosync_c1, "kyoo_autosync", "python, python3.12", "")
|
||||
}
|
||||
|
||||
Container_Boundary(transcoder, "transcoder") {
|
||||
Component(transcoder_c1, "kyoo_transcoder", "go, go", "Video Transcoder")
|
||||
}
|
||||
|
||||
Rel(user, backend_c2, "")
|
||||
Rel(backend_c1, backend_db1, "")
|
||||
Rel(backend_c2, backend_db1, "")
|
||||
Rel(backend_c2, backend_db2, "")
|
||||
Rel(backend_c2, transcoder_c1, "")
|
||||
Rel(backend_c2, backend_c3, "")
|
||||
Rel(backend_c2, emb_q2, "produces")
|
||||
Rel(backend_c2, emb_e1, "produces")
|
||||
Rel(backend_c2, emb_e2, "produces")
|
||||
Rel(emb_e1, emb_q1, "bound")
|
||||
Rel_Back(autosync_c1, emb_q1, "consumes")
|
||||
Rel_Back(scanner_c1, emb_q2, "consumes")
|
||||
Rel(scanner_c1, backend_c2, "")
|
||||
Rel(scanner_c2, backend_c2, "")
|
||||
Rel(frontend_c1, backend_c2, "")
|
||||
Rel(auth_c1, auth_db1, "")
|
||||
```
|
||||
|
||||
### Front
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="2")
|
||||
|
||||
title Component Diagram for Front
|
||||
|
||||
Person(user, "User")
|
||||
Container_Boundary(frontend, "front") {
|
||||
Component(frontend_c1, "kyoo_front", "typescript, node.js", "Static Content")
|
||||
}
|
||||
Container_Boundary(backend, "back") {
|
||||
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
|
||||
}
|
||||
|
||||
Rel(frontend_c1, backend_c2, "ssr")
|
||||
Rel(user, frontend_c1, "")
|
||||
```
|
||||
|
||||
### Scanner
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="3")
|
||||
|
||||
title Component Diagram for Scanner
|
||||
|
||||
Container_Boundary(media, "MediaLibrary") {
|
||||
Component_Ext(media_c1, "MediaShare", "Volume", "Read Only")
|
||||
}
|
||||
|
||||
Container_Boundary(content, "ContentDatabase") {
|
||||
Component_Ext(content_c1, "ContentProvider", "API", "tmdb or tvdb")
|
||||
}
|
||||
|
||||
Container_Boundary(scanner, "scanner") {
|
||||
Component(scanner_c2, "kyoo_scanner", "python, python3.12", "matcher")
|
||||
ComponentQueue(scanner_q1, "scanner", "RabbitMQ, Queue", "")
|
||||
Component(scanner_c1, "kyoo_scanner", "python, python3.12", "scanner")
|
||||
}
|
||||
|
||||
Container_Boundary(backend, "back") {
|
||||
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
|
||||
}
|
||||
|
||||
Container_Boundary(emb, "emb") {
|
||||
ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
|
||||
}
|
||||
|
||||
Rel(scanner_c1, scanner_q1, "produces")
|
||||
Rel(scanner_c1, media_c1, "watches")
|
||||
Rel(scanner_c1, backend_c2, "Fetch existing scans")
|
||||
Rel(scanner_c2, content_c1, "Fetch media data")
|
||||
Rel(scanner_c2, backend_c2, "Pushes media data")
|
||||
Rel_Back(scanner_c2, scanner_q1, "consumes")
|
||||
Rel_Back(scanner_c1, emb_q2, "consumes")
|
||||
Rel(backend_c2, emb_q2, "produces")
|
||||
```
|
||||
|
||||
### Transcoder
|
||||
#### Api
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="2")
|
||||
|
||||
title Component Diagram for Transcoder
|
||||
title Api Component Diagram
|
||||
|
||||
Person(user, "User")
|
||||
Container_Boundary(api, "api") {
|
||||
ComponentDb(api_db1, "kyoo", "Postgres", "")
|
||||
Component(api_c1, "kyoo_api", "TypeScript", "")
|
||||
Component(api_c2, "ApiMetadata", "S3/Volume", "Persistent. Distributed Metadata")
|
||||
}
|
||||
Container_Boundary(scanner, "scanner") {
|
||||
Component(scanner_c1, "kyoo_scanner", "Python", "")
|
||||
}
|
||||
|
||||
System_Boundary(external, "") {
|
||||
System_Ext(tracker, "ActivityTracker", "")
|
||||
System_Ext(content, "ContentDatabase", "")
|
||||
}
|
||||
|
||||
Container_Boundary(front, "front") {
|
||||
Component(front_c1, "kyoo_front", "TypeScript", "")
|
||||
}
|
||||
|
||||
Rel(user, api_c1, "auth via middleware")
|
||||
Rel(api_c1, api_db1, "")
|
||||
Rel(api_c1, api_c2, "")
|
||||
Rel(api_c1, content, "http(s) <br/> retries media images")
|
||||
Rel(api_c1, tracker, "")
|
||||
Rel(scanner_c1, api_c1, "auth via middleware")
|
||||
Rel(front_c1, api_c1, "http(s) SSR <br/> auth via middleware")
|
||||
```
|
||||
|
||||
#### Front
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="2")
|
||||
|
||||
title Front Component Diagram
|
||||
|
||||
Person(user, "User")
|
||||
Container_Boundary(front, "front") {
|
||||
Component(front_c1, "kyoo_front", "TypeScript", "")
|
||||
}
|
||||
|
||||
Container_Boundary(api, "api") {
|
||||
Component(api_c1, "kyoo_api", "TypeScript", "")
|
||||
}
|
||||
Rel(user, front_c1, "")
|
||||
Rel(front_c1, api_c1, "http(s) SSR <br/> auth via middleware")
|
||||
```
|
||||
|
||||
|
||||
#### Transcoder
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1")
|
||||
|
||||
title Transcoder Component Diagram
|
||||
|
||||
Person(user, "User")
|
||||
|
||||
Container_Boundary(transcoder, "transcoder") {
|
||||
Component(transcoder_c2, "TranscodeMetadata", "Volume", "Persistent. Distributed Metadata")
|
||||
Component(transcoder_c1, "kyoo_transcoder", "go, go", "Video Transcoder")
|
||||
ComponentDb(transcoder_db1, "gocoder", "Postgres", "")
|
||||
Component(transcoder_c1, "kyoo_transcoder", "Go", "")
|
||||
Component(transcoder_c2, "TranscodeMetadata", "S3/Volume", "Persistent. Distributed Metadata")
|
||||
Component(transcoder_c3, "TranscodeCache", "Volume", "Volatile. Local cache")
|
||||
}
|
||||
Container_Boundary(media, "MediaLibrary") {
|
||||
Component_Ext(media_c1, "MediaShare", "Volume", "Read Only")
|
||||
}
|
||||
Container_Boundary(backend, "back") {
|
||||
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
|
||||
|
||||
System_Boundary(external, "") {
|
||||
System_Ext(media, "MediaLibrary", "")
|
||||
}
|
||||
|
||||
Rel(transcoder_c1, media_c1, "mounts")
|
||||
Rel(user, transcoder_c1, "auth via middleware")
|
||||
Rel(transcoder_c1, media, "mounted to filesystem <br/> reads")
|
||||
Rel(transcoder_c1, transcoder_db1, "")
|
||||
Rel(transcoder_c1, transcoder_c2, "")
|
||||
Rel(transcoder_c1, transcoder_c3, "")
|
||||
Rel(backend_c2, transcoder_c1, "")
|
||||
```
|
||||
|
||||
|
||||
#### Scanner
|
||||
```mermaid
|
||||
C4Component
|
||||
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="2")
|
||||
|
||||
title Scanner Component Diagram
|
||||
|
||||
Person(user, "User")
|
||||
|
||||
Container_Boundary(api, "api") {
|
||||
Component(api_c1, "kyoo_api", "TypeScript", "")
|
||||
}
|
||||
|
||||
Container_Boundary(scanner, "scanner") {
|
||||
Component(scanner_c1, "kyoo_scanner", "Python", "")
|
||||
ComponentDb(scanner_db1, "scanner", "Postgres", "")
|
||||
}
|
||||
System_Boundary(external, "") {
|
||||
System_Ext(content, "ContentDatabase", "")
|
||||
System_Ext(media, "MediaLibrary", "")
|
||||
}
|
||||
|
||||
Rel(user, scanner_c1, "http(s) <br/> auth via middleware")
|
||||
Rel(scanner_c1, api_c1, "http(s) <br/> auth via middleware")
|
||||
Rel(scanner_c1, scanner_db1, "")
|
||||
Rel(scanner_c1, content, "http(s) <br/> gathers media metadata")
|
||||
Rel(scanner_c1, media, "mounted to filesystem <br/> watches")
|
||||
```
|
||||
2
OIDC.md
2
OIDC.md
@@ -45,7 +45,7 @@ Then, fill the following environment variables in your `.env` file:
|
||||
```env
|
||||
PUBLIC_URL=https://your-kyoo-instance.com
|
||||
OIDC_GOOGLE_NAME=Google
|
||||
OIDC_GOOGLE_LOGO=https://logo.clearbit.com/google.com
|
||||
OIDC_GOOGLE_LOGO=https://www.gstatic.com/marketing-cms/assets/images/d5/dc/cfe9ce8b4425b410b49b7f2dd3f3/g.webp=s200
|
||||
OIDC_GOOGLE_CLIENTID=<client-id> # the client ID you got from Google
|
||||
OIDC_GOOGLE_SECRET=<client-secret> # the client secret you got from Google
|
||||
OIDC_GOOGLE_AUTHORIZATION=https://accounts.google.com/o/oauth2/auth
|
||||
|
||||
@@ -39,10 +39,7 @@ Kyoo does not have a plugin system and aim to have every features built-in (see
|
||||
|
||||
## 📺 Clients
|
||||
|
||||
Kyoo currently supports Web and Android clients, with additional platforms being thought about. Rough estimates:
|
||||
* Today: Web & Android
|
||||
* Spring 2025: Chromecast
|
||||
* Summer 2025: Android-TV
|
||||
Kyoo currently supports Web and Android clients, with additional platforms being thought about.
|
||||
|
||||
Don't see your client? Kyoo is focused on adding features, but welcomes contributors! The frontend is built with React-Native and Expo. Come hang and develop with us on Discord.
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ AUTH_SERVER=http://auth:4568
|
||||
|
||||
IMAGES_PATH=./images
|
||||
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||
|
||||
# It is recommended to use the below PG environment variables when possible.
|
||||
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
|
||||
# The behavior of the below variables match what is documented here:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun AS builder
|
||||
FROM oven/bun:debian AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock .
|
||||
@@ -18,11 +18,13 @@ RUN bun build \
|
||||
--outfile server \
|
||||
./src/index.ts
|
||||
|
||||
FROM gcr.io/distroless/base
|
||||
FROM debian
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/server server
|
||||
COPY --from=builder /app/node_modules/@img /app/node_modules/@img
|
||||
COPY ./drizzle ./drizzle
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3567
|
||||
CMD ["./server"]
|
||||
CMD ["/app/server"]
|
||||
|
||||
357
api/bun.lock
357
api/bun.lock
@@ -1,150 +1,245 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "api",
|
||||
"dependencies": {
|
||||
"@elysiajs/opentelemetry": "^1.4.8",
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"@kubiks/otel-drizzle": "zoriya/drizzle-otel#build",
|
||||
"@types/bun": "^1.3.1",
|
||||
"blurhash": "^2.0.5",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"elysia": "^1.3.1",
|
||||
"jose": "^6.0.11",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"drizzle-orm": "0.44.7",
|
||||
"elysia": "^1.4.13",
|
||||
"jose": "^6.1.0",
|
||||
"node-addon-api": "^8.5.0",
|
||||
"parjs": "^1.3.9",
|
||||
"pg": "^8.16.0",
|
||||
"sharp": "^0.34.2",
|
||||
"pg": "^8.16.3",
|
||||
"sharp": "^0.34.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@types/pg": "^8.15.2",
|
||||
"bun-types": "^1.2.14",
|
||||
"node-addon-api": "^8.3.1",
|
||||
"@biomejs/biome": "2.3.7",
|
||||
"@types/pg": "^8.15.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"drizzle-orm@0.43.1": "patches/drizzle-orm@0.43.1.patch",
|
||||
"drizzle-orm@0.44.7": "patches/drizzle-orm@0.44.7.patch",
|
||||
},
|
||||
"packages": {
|
||||
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.7", "@biomejs/cli-darwin-x64": "2.3.7", "@biomejs/cli-linux-arm64": "2.3.7", "@biomejs/cli-linux-arm64-musl": "2.3.7", "@biomejs/cli-linux-x64": "2.3.7", "@biomejs/cli-linux-x64-musl": "2.3.7", "@biomejs/cli-win32-arm64": "2.3.7", "@biomejs/cli-win32-x64": "2.3.7" }, "bin": { "biome": "bin/biome" } }, "sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.8", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-c9unbcdXfehExCv1GsiTCfos5SyIAyDwP7apcMeXmUMBaJZiAYMfiEH8RFFFIfIHJHC/xlNJzUPodkcUaaoJJQ=="],
|
||||
|
||||
"@elysiajs/swagger": ["@elysiajs/swagger@github:zoriya/elysia-swagger#f88fbc7", { "dependencies": { "@scalar/themes": "^0.9.81", "@scalar/types": "^0.1.3", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "zoriya-elysia-swagger-f88fbc7"],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
|
||||
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ=="],
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.3", "", { "os": "android", "cpu": "arm" }, "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A=="],
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.3", "", { "os": "android", "cpu": "arm64" }, "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ=="],
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.3", "", { "os": "android", "cpu": "x64" }, "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ=="],
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w=="],
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A=="],
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw=="],
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q=="],
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.3", "", { "os": "linux", "cpu": "arm" }, "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ=="],
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A=="],
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw=="],
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g=="],
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag=="],
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg=="],
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA=="],
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ=="],
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.3", "", { "os": "linux", "cpu": "x64" }, "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA=="],
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.3", "", { "os": "none", "cpu": "arm64" }, "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA=="],
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.3", "", { "os": "none", "cpu": "x64" }, "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g=="],
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ=="],
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w=="],
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA=="],
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ=="],
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew=="],
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg=="],
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="],
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="],
|
||||
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="],
|
||||
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="],
|
||||
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="],
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="],
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="],
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="],
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="],
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="],
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="],
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="],
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="],
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="],
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="],
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="],
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="],
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="],
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="],
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="],
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="],
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.4", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="],
|
||||
|
||||
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||
|
||||
"@kubiks/otel-drizzle": ["@kubiks/otel-drizzle@github:zoriya/drizzle-otel#cc1d59b", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "drizzle-orm": ">=0.28.0" } }, "zoriya-drizzle-otel-cc1d59b"],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="],
|
||||
|
||||
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="],
|
||||
|
||||
"@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
|
||||
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="],
|
||||
|
||||
"@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g=="],
|
||||
|
||||
"@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA=="],
|
||||
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="],
|
||||
|
||||
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="],
|
||||
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="],
|
||||
|
||||
"@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg=="],
|
||||
|
||||
"@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg=="],
|
||||
|
||||
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="],
|
||||
|
||||
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="],
|
||||
|
||||
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="],
|
||||
|
||||
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
|
||||
|
||||
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
|
||||
|
||||
@@ -158,45 +253,65 @@
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.15.2", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-+BKxo5mM6+/A1soSHBI7ufUglqYXntChLDyTbvcAn1Lawi9J7J9Ok3jt6w7I0+T/UDJ4CyhHk66+GZbwmkYxSg=="],
|
||||
"@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||
|
||||
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
|
||||
|
||||
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"blurhash": ["blurhash@2.0.5", "", {}, "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
|
||||
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||
|
||||
"char-info": ["char-info@0.3.5", "", { "dependencies": { "node-interval-tree": "^1.3.3" } }, "sha512-gRslEBFEcuLMGLNO1EFIrdN1MMUfO+aqa7y8iWzNyAzB3mYKnTIvP+ioW3jpyeEvqA5WapVLIPINGtFjEIH4cQ=="],
|
||||
|
||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.1", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.2", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q=="],
|
||||
"drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.43.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA=="],
|
||||
"drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
|
||||
|
||||
"elysia": ["elysia@1.3.1", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-En41P6cDHcHtQ0nvfsn9ayB+8ahQJqG1nzvPX8FVZjOriFK/RtZPQBtXMfZDq/AsVIk7JFZGFEtAVEmztNJVhQ=="],
|
||||
"elysia": ["elysia@1.4.13", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-6QaWQEm7QN1UCo1TPpEjaRJPHUmnM7R29y6LY224frDGk5PrpAnWmdHkoZxkcv+JRWp1j2ROr2IHbxHbG/jRjw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.3", "@esbuild/android-arm": "0.25.3", "@esbuild/android-arm64": "0.25.3", "@esbuild/android-x64": "0.25.3", "@esbuild/darwin-arm64": "0.25.3", "@esbuild/darwin-x64": "0.25.3", "@esbuild/freebsd-arm64": "0.25.3", "@esbuild/freebsd-x64": "0.25.3", "@esbuild/linux-arm": "0.25.3", "@esbuild/linux-arm64": "0.25.3", "@esbuild/linux-ia32": "0.25.3", "@esbuild/linux-loong64": "0.25.3", "@esbuild/linux-mips64el": "0.25.3", "@esbuild/linux-ppc64": "0.25.3", "@esbuild/linux-riscv64": "0.25.3", "@esbuild/linux-s390x": "0.25.3", "@esbuild/linux-x64": "0.25.3", "@esbuild/netbsd-arm64": "0.25.3", "@esbuild/netbsd-x64": "0.25.3", "@esbuild/openbsd-arm64": "0.25.3", "@esbuild/openbsd-x64": "0.25.3", "@esbuild/sunos-x64": "0.25.3", "@esbuild/win32-arm64": "0.25.3", "@esbuild/win32-ia32": "0.25.3", "@esbuild/win32-x64": "0.25.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q=="],
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
@@ -204,59 +319,81 @@
|
||||
|
||||
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
"import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="],
|
||||
|
||||
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
||||
|
||||
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||
|
||||
"module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@8.3.1", "", {}, "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA=="],
|
||||
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
|
||||
|
||||
"node-interval-tree": ["node-interval-tree@1.3.3", "", { "dependencies": { "shallowequal": "^1.0.2" } }, "sha512-K9vk96HdTK5fEipJwxSvIIqwTqr4e3HRJeJrNxBSeVMNSC/JWARRaX7etOLOuTmrRMeOI/K5TCJu3aWIwZiNTw=="],
|
||||
|
||||
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"parjs": ["parjs@1.3.9", "", { "dependencies": { "char-info": "0.3.*" } }, "sha512-zmQhbzWM3M391tjwTGvNvvtoT8rRE/bBTjw6+54g8ANaPpnyekDF1d8q5tzN4kxmVud82cNj8zSd+uxSL4LE0A=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
|
||||
|
||||
"pg": ["pg@8.16.0", "", { "dependencies": { "pg-connection-string": "^2.9.0", "pg-pool": "^3.10.0", "pg-protocol": "^1.10.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.5" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg=="],
|
||||
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.2.5", "", {}, "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg=="],
|
||||
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.9.0", "", {}, "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ=="],
|
||||
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.10.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA=="],
|
||||
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.10.0", "", {}, "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q=="],
|
||||
|
||||
"pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
|
||||
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||
|
||||
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
|
||||
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
@@ -264,9 +401,9 @@
|
||||
|
||||
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
|
||||
|
||||
"sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="],
|
||||
"sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
@@ -274,27 +411,39 @@
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
|
||||
|
||||
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
"pg/pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
@@ -339,13 +488,5 @@
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
|
||||
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||
|
||||
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
23
api/devspace.yaml
Normal file
23
api/devspace.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: v2beta1
|
||||
name: api
|
||||
dev:
|
||||
api:
|
||||
imageSelector: ghcr.io/zoriya/kyoo_api
|
||||
devImage: docker.io/oven/bun:latest
|
||||
workingDir: /app
|
||||
sync:
|
||||
- path: .:/app
|
||||
excludePaths:
|
||||
- node_modules
|
||||
startContainer: true
|
||||
onUpload:
|
||||
exec:
|
||||
- command: bun install --frozen-lockfile
|
||||
onChange:
|
||||
- "./bun.lock"
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- "bun install && bun dev"
|
||||
ports:
|
||||
- port: "3567"
|
||||
3
api/drizzle/0023_mqueue-priority.sql
Normal file
3
api/drizzle/0023_mqueue-priority.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "kyoo"."history" ALTER COLUMN "time" SET DEFAULT 0;--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."history" ALTER COLUMN "time" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."mqueue" ADD COLUMN "priority" integer DEFAULT 0 NOT NULL;
|
||||
1
api/drizzle/0024_fix-season-count.sql
Normal file
1
api/drizzle/0024_fix-season-count.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "kyoo"."seasons" ALTER COLUMN "entries_count" SET DEFAULT 0;
|
||||
1880
api/drizzle/meta/0023_snapshot.json
Normal file
1880
api/drizzle/meta/0023_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1881
api/drizzle/meta/0024_snapshot.json
Normal file
1881
api/drizzle/meta/0024_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -162,6 +162,20 @@
|
||||
"when": 1752446736231,
|
||||
"tag": "0022_seasons-count",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "7",
|
||||
"when": 1763924097229,
|
||||
"tag": "0023_mqueue-priority",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "7",
|
||||
"when": 1763932730557,
|
||||
"tag": "0024_fix-season-count",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,24 +9,26 @@
|
||||
"format": "biome check --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/opentelemetry": "^1.4.8",
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"@kubiks/otel-drizzle": "zoriya/drizzle-otel#build",
|
||||
"@types/bun": "^1.3.1",
|
||||
"blurhash": "^2.0.5",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"elysia": "^1.3.1",
|
||||
"jose": "^6.0.11",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"drizzle-orm": "0.44.7",
|
||||
"elysia": "^1.4.13",
|
||||
"jose": "^6.1.0",
|
||||
"node-addon-api": "^8.5.0",
|
||||
"parjs": "^1.3.9",
|
||||
"pg": "^8.16.0",
|
||||
"sharp": "^0.34.2"
|
||||
"pg": "^8.16.3",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@types/pg": "^8.15.2",
|
||||
"bun-types": "^1.2.14",
|
||||
"node-addon-api": "^8.3.1"
|
||||
"@biomejs/biome": "2.3.7",
|
||||
"@types/pg": "^8.15.5"
|
||||
},
|
||||
"module": "src/index.js",
|
||||
"patchedDependencies": {
|
||||
"drizzle-orm@0.43.1": "patches/drizzle-orm@0.43.1.patch"
|
||||
"drizzle-orm@0.44.7": "patches/drizzle-orm@0.44.7.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/pg-core/dialect.cjs b/pg-core/dialect.cjs
|
||||
index a0ef03142f21d319376bc50070ff7fdcd4d18132..45fc94e5a7c3fa4c201e636dd227122164e1bd02 100644
|
||||
index d776a1fb503f35b5e53c6d3c1c086efa8230ea94..86541bf408e4955029c65be59d7b8ec98bb6e914 100644
|
||||
--- a/pg-core/dialect.cjs
|
||||
+++ b/pg-core/dialect.cjs
|
||||
@@ -348,7 +348,14 @@ class PgDialect {
|
||||
@@ -347,7 +347,14 @@ class PgDialect {
|
||||
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
|
||||
const valuesSqlList = [];
|
||||
const columns = table[import_table2.Table.Symbol.Columns];
|
||||
@@ -19,30 +19,30 @@ index a0ef03142f21d319376bc50070ff7fdcd4d18132..45fc94e5a7c3fa4c201e636dd2271221
|
||||
([, column]) => import_sql2.sql.identifier(this.casing.getColumnCasing(column))
|
||||
);
|
||||
diff --git a/pg-core/dialect.js b/pg-core/dialect.js
|
||||
index 120aaed9c3e4ae0a24653893379b98506c866f6f..48df463c0a6d5864fe2c324c8f86432860e50e00 100644
|
||||
index 74a16c9e86fe3a89ced32af44af0a72f1cf43cf6..08f820d46a040c315fed40b8b36d94d094174425 100644
|
||||
--- a/pg-core/dialect.js
|
||||
+++ b/pg-core/dialect.js
|
||||
@@ -346,7 +346,14 @@ class PgDialect {
|
||||
@@ -345,7 +345,14 @@ class PgDialect {
|
||||
buildInsertQuery({ table, values: valuesOrSelect, onConflict, returning, withList, select, overridingSystemValue_ }) {
|
||||
const valuesSqlList = [];
|
||||
const columns = table[Table.Symbol.Columns];
|
||||
- const colEntries = Object.entries(columns).filter(([_, col]) => !col.shouldDisableInsert());
|
||||
+ let colEntries = Object.entries(columns);
|
||||
+ colEntries = select && !is(valuesOrSelect, SQL)
|
||||
+ ? Object
|
||||
+ .keys(valuesOrSelect._.selectedFields)
|
||||
+ .map((key) => [key, columns[key]])
|
||||
+ : overridingSystemValue_
|
||||
+ ? colEntries
|
||||
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
|
||||
+ let colEntries = Object.entries(columns);
|
||||
+ colEntries = select && !is(valuesOrSelect, SQL)
|
||||
+ ? Object
|
||||
+ .keys(valuesOrSelect._.selectedFields)
|
||||
+ .map((key) => [key, columns[key]])
|
||||
+ : overridingSystemValue_
|
||||
+ ? colEntries
|
||||
+ : colEntries.filter(([_, col]) => !col.shouldDisableInsert());
|
||||
const insertOrder = colEntries.map(
|
||||
([, column]) => sql.identifier(this.casing.getColumnCasing(column))
|
||||
);
|
||||
diff --git a/pg-core/query-builders/insert.cjs b/pg-core/query-builders/insert.cjs
|
||||
index 08bb0d7485ebf997e3f081e2254ea8fd8bc20f65..20c8036374a1f25f7c5880c40e8d3c42c05f3eee 100644
|
||||
index 22e0a4b4ad7ac64b065fc540416c6ed15ff4336b..fd590a6a3feb48c9f1894a0619b09ca6a5d22a5c 100644
|
||||
--- a/pg-core/query-builders/insert.cjs
|
||||
+++ b/pg-core/query-builders/insert.cjs
|
||||
@@ -75,11 +75,6 @@ class PgInsertBuilder {
|
||||
@@ -76,11 +76,6 @@ class PgInsertBuilder {
|
||||
}
|
||||
select(selectQuery) {
|
||||
const select = typeof selectQuery === "function" ? selectQuery(new import_query_builder.QueryBuilder()) : selectQuery;
|
||||
@@ -55,10 +55,10 @@ index 08bb0d7485ebf997e3f081e2254ea8fd8bc20f65..20c8036374a1f25f7c5880c40e8d3c42
|
||||
}
|
||||
}
|
||||
diff --git a/pg-core/query-builders/insert.js b/pg-core/query-builders/insert.js
|
||||
index 0fc8eeb80f4a5512f6c84f3d596832623a33b748..998e2ab0bfe3f322bf268a01f71ebd06c57d4d07 100644
|
||||
index 60a8bb0d1c22b890bd8fbf4c85d5df41ca42444c..8754d0f2923f905816016c42f339c3e9097b4128 100644
|
||||
--- a/pg-core/query-builders/insert.js
|
||||
+++ b/pg-core/query-builders/insert.js
|
||||
@@ -51,11 +51,6 @@ class PgInsertBuilder {
|
||||
@@ -52,11 +52,6 @@ class PgInsertBuilder {
|
||||
}
|
||||
select(selectQuery) {
|
||||
const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery;
|
||||
@@ -4,7 +4,7 @@ pkgs.mkShell {
|
||||
bun
|
||||
biome
|
||||
# for psql to debug from the cli
|
||||
postgresql_15
|
||||
postgresql_18
|
||||
# to build libvips (for sharp)
|
||||
nodejs
|
||||
node-gyp
|
||||
|
||||
@@ -73,7 +73,7 @@ export const auth = new Elysia({ name: "auth" })
|
||||
.macro({
|
||||
permissions(perms: string[]) {
|
||||
return {
|
||||
beforeHandle: ({ jwt, status }) => {
|
||||
beforeHandle: function permissionCheck({ jwt, status }) {
|
||||
for (const perm of perms) {
|
||||
if (!jwt!.permissions.includes(perm)) {
|
||||
return status(403, {
|
||||
|
||||
@@ -13,11 +13,23 @@ import { series } from "./controllers/shows/series";
|
||||
import { showsH } from "./controllers/shows/shows";
|
||||
import { staffH } from "./controllers/staff";
|
||||
import { studiosH } from "./controllers/studios";
|
||||
import { videosH } from "./controllers/videos";
|
||||
import { videosReadH, videosWriteH } from "./controllers/videos";
|
||||
import { db } from "./db";
|
||||
import type { KError } from "./models/error";
|
||||
import { otel } from "./otel";
|
||||
|
||||
export const base = new Elysia({ name: "base" })
|
||||
.onError(({ code, error }) => {
|
||||
// sometimes elysia as an unknown code when throwing errors
|
||||
if (code === "UNKNOWN") {
|
||||
try {
|
||||
const details = JSON.parse(error.message);
|
||||
if (details?.code === "KError") {
|
||||
const { code, ...ret } = details;
|
||||
return ret;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (code === "VALIDATION") {
|
||||
const details = JSON.parse(error.message);
|
||||
if (details.code === "KError") {
|
||||
@@ -34,30 +46,54 @@ export const base = new Elysia({ name: "base" })
|
||||
details: details,
|
||||
} as KError;
|
||||
}
|
||||
if (code === "INTERNAL_SERVER_ERROR") {
|
||||
console.error(error);
|
||||
return {
|
||||
status: 500,
|
||||
message: error.message,
|
||||
details: error,
|
||||
} as KError;
|
||||
}
|
||||
if (code === "NOT_FOUND") {
|
||||
return error;
|
||||
}
|
||||
console.error(code, error);
|
||||
return error;
|
||||
return {
|
||||
status: 500,
|
||||
message: "message" in error ? (error?.message ?? code) : code,
|
||||
details: error,
|
||||
} as KError;
|
||||
})
|
||||
.get("/health", () => ({ status: "healthy" }) as const, {
|
||||
detail: { description: "Check if the api is healthy." },
|
||||
response: { 200: t.Object({ status: t.Literal("healthy") }) },
|
||||
})
|
||||
.as("scoped");
|
||||
.get(
|
||||
"/ready",
|
||||
async ({ status }) => {
|
||||
try {
|
||||
await db.execute("select 1");
|
||||
return { status: "healthy", database: "healthy" } as const;
|
||||
} catch (e) {
|
||||
return status(500, {
|
||||
status: "unhealthy",
|
||||
database: e,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
detail: { description: "Check if the api is healthy." },
|
||||
response: {
|
||||
200: t.Object({
|
||||
status: t.Literal("healthy"),
|
||||
database: t.Literal("healthy"),
|
||||
}),
|
||||
500: t.Object({
|
||||
status: t.Literal("unhealthy"),
|
||||
database: t.Any(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
.as("global");
|
||||
|
||||
export const prefix = "/api";
|
||||
export const handlers = new Elysia({ prefix })
|
||||
.use(base)
|
||||
.use(auth)
|
||||
.use(otel)
|
||||
.guard(
|
||||
{
|
||||
// Those are not applied for now. See https://github.com/elysiajs/elysia/issues/1139
|
||||
@@ -84,7 +120,8 @@ export const handlers = new Elysia({ prefix })
|
||||
.use(imagesH)
|
||||
.use(watchlistH)
|
||||
.use(historyH)
|
||||
.use(nextup),
|
||||
.use(nextup)
|
||||
.use(videosReadH),
|
||||
)
|
||||
.guard(
|
||||
{
|
||||
@@ -98,5 +135,5 @@ export const handlers = new Elysia({ prefix })
|
||||
// },
|
||||
permissions: ["core.write"],
|
||||
},
|
||||
(app) => app.use(videosH).use(seed),
|
||||
(app) => app.use(videosWriteH).use(seed),
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export const entryProgressQ = db
|
||||
})
|
||||
.from(history)
|
||||
.leftJoin(videos, eq(history.videoPk, videos.pk))
|
||||
.leftJoin(profiles, eq(history.profilePk, profiles.pk))
|
||||
.innerJoin(profiles, eq(history.profilePk, profiles.pk))
|
||||
.where(eq(profiles.id, sql.placeholder("userId")))
|
||||
.orderBy(history.entryPk, desc(history.playedDate))
|
||||
.as("progress");
|
||||
@@ -157,7 +157,7 @@ export const mapProgress = ({ aliased }: { aliased: boolean }) => {
|
||||
const ret = {
|
||||
time: coalesce(time, sql<number>`0`),
|
||||
percent: coalesce(percent, sql<number>`0`),
|
||||
playedDate: sql`to_char(${playedDate}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
playedDate: sql<string>`to_char(${playedDate}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
videoId: sql<string>`${videoId}`,
|
||||
};
|
||||
if (!aliased) return ret;
|
||||
@@ -256,7 +256,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub },
|
||||
status,
|
||||
@@ -294,7 +294,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
userId: sub,
|
||||
})) as Entry[];
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get entries of a serie" },
|
||||
@@ -338,6 +338,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter },
|
||||
request: { url },
|
||||
headers,
|
||||
jwt: { sub },
|
||||
status,
|
||||
}) => {
|
||||
@@ -373,7 +374,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
userId: sub,
|
||||
})) as Extra[];
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get extras of a serie" },
|
||||
@@ -410,6 +411,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
async ({
|
||||
query: { limit, after, query, filter },
|
||||
request: { url },
|
||||
headers,
|
||||
jwt: { sub },
|
||||
}) => {
|
||||
const sort = newsSort;
|
||||
@@ -427,7 +429,7 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
||||
userId: sub,
|
||||
})) as Entry[];
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get new movies/episodes added recently." },
|
||||
|
||||
@@ -27,9 +27,9 @@ function getRedirectToImageHandler({ filter }: { filter?: SQL }) {
|
||||
status,
|
||||
redirect,
|
||||
}: {
|
||||
params: { id: string; image: "poster" | "thumbnail" | "banner" | "logo" };
|
||||
params: { id?: string; image: "poster" | "thumbnail" | "banner" | "logo" };
|
||||
headers: { "accept-language": string };
|
||||
query: { quality: "high" | "medium" | "low" };
|
||||
query: { quality?: "high" | "medium" | "low" };
|
||||
set: Context["set"];
|
||||
status: Context["status"];
|
||||
redirect: Context["redirect"];
|
||||
@@ -212,12 +212,9 @@ export const imagesH = new Elysia({ tags: ["images"] })
|
||||
},
|
||||
)
|
||||
.guard({
|
||||
headers: t.Object(
|
||||
{
|
||||
"accept-language": AcceptLanguage(),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
),
|
||||
headers: t.Object({
|
||||
"accept-language": AcceptLanguage(),
|
||||
}),
|
||||
})
|
||||
.get(
|
||||
"/studios/:id/logo",
|
||||
@@ -307,6 +304,9 @@ export const imagesH = new Elysia({ tags: ["images"] })
|
||||
description: "The type of image to retrive.",
|
||||
}),
|
||||
}),
|
||||
headers: t.Object({
|
||||
"accept-language": AcceptLanguage(),
|
||||
}),
|
||||
})
|
||||
.get(
|
||||
"/movies/:id/:image",
|
||||
|
||||
@@ -67,7 +67,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
"/profiles/me/history",
|
||||
async ({
|
||||
query: { sort, filter, query, limit, after },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub },
|
||||
}) => {
|
||||
@@ -87,7 +87,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
progressQ: historyProgressQ,
|
||||
})) as Entry[];
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
@@ -109,7 +109,11 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
async ({
|
||||
params: { id },
|
||||
query: { sort, filter, query, limit, after },
|
||||
headers: { "accept-language": languages, authorization },
|
||||
headers: {
|
||||
"accept-language": languages,
|
||||
authorization,
|
||||
...headers
|
||||
},
|
||||
request: { url },
|
||||
status,
|
||||
}) => {
|
||||
@@ -132,7 +136,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
progressQ: historyProgressQ,
|
||||
})) as Entry[];
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
|
||||
@@ -69,7 +69,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
||||
"/profiles/me/nextup",
|
||||
async ({
|
||||
query: { sort, filter, query, limit, after },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub },
|
||||
}) => {
|
||||
@@ -124,7 +124,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
||||
.limit(limit)
|
||||
.execute({ userId: sub });
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
|
||||
@@ -142,7 +142,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
|
||||
"/profiles/me/watchlist",
|
||||
async ({
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub, settings },
|
||||
}) => {
|
||||
@@ -162,7 +162,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
|
||||
relations: ["nextEntry"],
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all movies/series in your watchlist" },
|
||||
@@ -195,7 +195,11 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
jwt: { settings },
|
||||
headers: { "accept-language": languages, authorization },
|
||||
headers: {
|
||||
"accept-language": languages,
|
||||
authorization,
|
||||
...headers
|
||||
},
|
||||
request: { url },
|
||||
status,
|
||||
}) => {
|
||||
@@ -218,7 +222,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
|
||||
relations: ["nextEntry"],
|
||||
userId: uInfo.id,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const seasonsH = new Elysia({ tags: ["series"] })
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
status,
|
||||
}) => {
|
||||
@@ -110,7 +110,7 @@ export const seasonsH = new Elysia({ tags: ["series"] })
|
||||
)
|
||||
.limit(limit);
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get seasons of a serie" },
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { getCurrentSpan, setAttributes } from "@elysiajs/opentelemetry";
|
||||
import { SpanStatusCode } from "@opentelemetry/api";
|
||||
import { encode } from "blurhash";
|
||||
import { and, eq, is, lt, type SQL, sql } from "drizzle-orm";
|
||||
import { PgColumn, type PgTable } from "drizzle-orm/pg-core";
|
||||
@@ -7,13 +9,15 @@ import type { PoolClient } from "pg";
|
||||
import sharp from "sharp";
|
||||
import { db, type Transaction } from "~/db";
|
||||
import { mqueue } from "~/db/schema/mqueue";
|
||||
import { unnestValues } from "~/db/utils";
|
||||
import type { Image } from "~/models/utils";
|
||||
import { record } from "~/otel";
|
||||
import { getFile } from "~/utils";
|
||||
|
||||
export const imageDir = process.env.IMAGES_PATH ?? "./images";
|
||||
export const imageDir = process.env.IMAGES_PATH ?? "/images";
|
||||
export const defaultBlurhash = "000000";
|
||||
|
||||
type ImageTask = {
|
||||
export type ImageTask = {
|
||||
id: string;
|
||||
url: string;
|
||||
table: string;
|
||||
@@ -23,12 +27,12 @@ type ImageTask = {
|
||||
// this will only push a task to the image downloader service and not download it instantly.
|
||||
// this is both done to prevent too many requests to be sent at once and to make sure POST
|
||||
// requests are not blocked by image downloading or blurhash calculation
|
||||
export const enqueueOptImage = async (
|
||||
tx: Transaction,
|
||||
export const enqueueOptImage = (
|
||||
imgQueue: ImageTask[],
|
||||
img:
|
||||
| { url: string | null; column: PgColumn }
|
||||
| { url: string | null; table: PgTable; column: SQL },
|
||||
): Promise<Image | null> => {
|
||||
): Image | null => {
|
||||
if (!img.url) return null;
|
||||
|
||||
const hasher = new Bun.CryptoHasher("sha256");
|
||||
@@ -64,11 +68,8 @@ export const enqueueOptImage = async (
|
||||
table: db.dialect.sqlToQuery(sql`${img.column.table}`).sql,
|
||||
column: sql.identifier(img.column.name).value,
|
||||
};
|
||||
await tx.insert(mqueue).values({
|
||||
kind: "image",
|
||||
message,
|
||||
});
|
||||
await tx.execute(sql`notify kyoo_image`);
|
||||
|
||||
imgQueue.push(message);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -77,43 +78,21 @@ export const enqueueOptImage = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const processImages = async () => {
|
||||
async function processOne() {
|
||||
return await db.transaction(async (tx) => {
|
||||
const [item] = await tx
|
||||
.select()
|
||||
.from(mqueue)
|
||||
.for("update", { skipLocked: true })
|
||||
.where(and(eq(mqueue.kind, "image"), lt(mqueue.attempt, 5)))
|
||||
.orderBy(mqueue.attempt, mqueue.createdAt)
|
||||
.limit(1);
|
||||
|
||||
if (!item) return false;
|
||||
|
||||
const img = item.message as ImageTask;
|
||||
try {
|
||||
const blurhash = await downloadImage(img.id, img.url);
|
||||
const ret: Image = { id: img.id, source: img.url, blurhash };
|
||||
|
||||
const table = sql.raw(img.table);
|
||||
const column = sql.raw(img.column);
|
||||
|
||||
await tx.execute(sql`
|
||||
update ${table} set ${column} = ${ret} where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
|
||||
`);
|
||||
|
||||
await tx.delete(mqueue).where(eq(mqueue.id, item.id));
|
||||
} catch (err: any) {
|
||||
console.error("Failed to download image", img.url, err.message);
|
||||
await tx
|
||||
.update(mqueue)
|
||||
.set({ attempt: sql`${mqueue.attempt}+1` })
|
||||
.where(eq(mqueue.id, item.id));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
export const flushImageQueue = record(
|
||||
"enqueueImages",
|
||||
async (tx: Transaction, imgQueue: ImageTask[], priority: number) => {
|
||||
if (!imgQueue.length) return;
|
||||
await tx.insert(mqueue).select(
|
||||
unnestValues(
|
||||
imgQueue.map((x) => ({ kind: "image", message: x, priority })),
|
||||
mqueue,
|
||||
),
|
||||
);
|
||||
await tx.execute(sql`notify kyoo_image`);
|
||||
},
|
||||
);
|
||||
|
||||
export const processImages = record("processImages", async () => {
|
||||
let running = false;
|
||||
async function processAll() {
|
||||
if (running) return;
|
||||
@@ -136,7 +115,50 @@ export const processImages = async () => {
|
||||
// start processing old tasks
|
||||
await processAll();
|
||||
return () => client.release(true);
|
||||
};
|
||||
});
|
||||
|
||||
const processOne = record("download", async () => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const [item] = await tx
|
||||
.select()
|
||||
.from(mqueue)
|
||||
.for("update", { skipLocked: true })
|
||||
.where(and(eq(mqueue.kind, "image"), lt(mqueue.attempt, 5)))
|
||||
.orderBy(mqueue.priority, mqueue.attempt, mqueue.createdAt)
|
||||
.limit(1);
|
||||
|
||||
if (!item) return false;
|
||||
|
||||
const img = item.message as ImageTask;
|
||||
setAttributes({ "item.url": img.url });
|
||||
try {
|
||||
const blurhash = await downloadImage(img.id, img.url);
|
||||
const ret: Image = { id: img.id, source: img.url, blurhash };
|
||||
|
||||
const table = sql.raw(img.table);
|
||||
const column = sql.raw(img.column);
|
||||
|
||||
await tx.execute(sql`
|
||||
update ${table} set ${column} = ${ret}
|
||||
where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
|
||||
`);
|
||||
|
||||
await tx.delete(mqueue).where(eq(mqueue.id, item.id));
|
||||
} catch (err: any) {
|
||||
const span = getCurrentSpan();
|
||||
if (span) {
|
||||
span.recordException(err);
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
}
|
||||
console.error("Failed to download image", img.url, err.message);
|
||||
await tx
|
||||
.update(mqueue)
|
||||
.set({ attempt: sql`${mqueue.attempt}+1` })
|
||||
.where(eq(mqueue.id, item.id));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
async function downloadImage(id: string, url: string): Promise<string> {
|
||||
const low = await getFile(path.join(imageDir, `${id}.low.jpg`))
|
||||
|
||||
@@ -5,79 +5,89 @@ import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import type { SeedCollection } from "~/models/collections";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
import type { SeedSerie } from "~/models/serie";
|
||||
import { enqueueOptImage } from "../images";
|
||||
import { record } from "~/otel";
|
||||
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
|
||||
|
||||
type ShowTrans = typeof showTranslations.$inferInsert;
|
||||
|
||||
export const insertCollection = async (
|
||||
collection: SeedCollection | undefined,
|
||||
show: (({ kind: "movie" } & SeedMovie) | ({ kind: "serie" } & SeedSerie)) & {
|
||||
nextRefresh: string;
|
||||
export const insertCollection = record(
|
||||
"insertCollection",
|
||||
async (
|
||||
collection: SeedCollection | undefined,
|
||||
show: (
|
||||
| ({ kind: "movie" } & SeedMovie)
|
||||
| ({ kind: "serie" } & SeedSerie)
|
||||
) & {
|
||||
nextRefresh: string;
|
||||
},
|
||||
) => {
|
||||
if (!collection) return null;
|
||||
const { translations, ...col } = collection;
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const imgQueue: ImageTask[] = [];
|
||||
const [ret] = await tx
|
||||
.insert(shows)
|
||||
.values({
|
||||
kind: "collection",
|
||||
status: "unknown",
|
||||
startAir: show.kind === "movie" ? show.airDate : show.startAir,
|
||||
endAir: show.kind === "movie" ? show.airDate : show.endAir,
|
||||
nextRefresh: show.nextRefresh,
|
||||
entriesCount: 0,
|
||||
original: {} as any,
|
||||
...col,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: shows.slug,
|
||||
set: {
|
||||
...conflictUpdateAllExcept(shows, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
"startAir",
|
||||
"endAir",
|
||||
]),
|
||||
startAir: sql`least(${shows.startAir}, excluded.start_air)`,
|
||||
endAir: sql`greatest(${shows.endAir}, excluded.end_air)`,
|
||||
},
|
||||
})
|
||||
.returning({ pk: shows.pk, id: shows.id, slug: shows.slug });
|
||||
|
||||
const trans: ShowTrans[] = Object.entries(translations).map(
|
||||
([lang, tr]) => ({
|
||||
pk: ret.pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
poster: enqueueOptImage(imgQueue, {
|
||||
url: tr.poster,
|
||||
column: showTranslations.poster,
|
||||
}),
|
||||
thumbnail: enqueueOptImage(imgQueue, {
|
||||
url: tr.thumbnail,
|
||||
column: showTranslations.thumbnail,
|
||||
}),
|
||||
logo: enqueueOptImage(imgQueue, {
|
||||
url: tr.logo,
|
||||
column: showTranslations.logo,
|
||||
}),
|
||||
banner: enqueueOptImage(imgQueue, {
|
||||
url: tr.banner,
|
||||
column: showTranslations.banner,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await flushImageQueue(tx, imgQueue, 100);
|
||||
// we can't unnest values here because show translations contains arrays.
|
||||
await tx
|
||||
.insert(showTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [showTranslations.pk, showTranslations.language],
|
||||
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
},
|
||||
) => {
|
||||
if (!collection) return null;
|
||||
const { translations, ...col } = collection;
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const [ret] = await tx
|
||||
.insert(shows)
|
||||
.values({
|
||||
kind: "collection",
|
||||
status: "unknown",
|
||||
startAir: show.kind === "movie" ? show.airDate : show.startAir,
|
||||
endAir: show.kind === "movie" ? show.airDate : show.endAir,
|
||||
nextRefresh: show.nextRefresh,
|
||||
entriesCount: 0,
|
||||
original: {} as any,
|
||||
...col,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: shows.slug,
|
||||
set: {
|
||||
...conflictUpdateAllExcept(shows, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
"startAir",
|
||||
"endAir",
|
||||
]),
|
||||
startAir: sql`least(${shows.startAir}, excluded.start_air)`,
|
||||
endAir: sql`greatest(${shows.endAir}, excluded.end_air)`,
|
||||
},
|
||||
})
|
||||
.returning({ pk: shows.pk, id: shows.id, slug: shows.slug });
|
||||
|
||||
const trans: ShowTrans[] = await Promise.all(
|
||||
Object.entries(translations).map(async ([lang, tr]) => ({
|
||||
pk: ret.pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
poster: await enqueueOptImage(tx, {
|
||||
url: tr.poster,
|
||||
column: showTranslations.poster,
|
||||
}),
|
||||
thumbnail: await enqueueOptImage(tx, {
|
||||
url: tr.thumbnail,
|
||||
column: showTranslations.thumbnail,
|
||||
}),
|
||||
logo: await enqueueOptImage(tx, {
|
||||
url: tr.logo,
|
||||
column: showTranslations.logo,
|
||||
}),
|
||||
banner: await enqueueOptImage(tx, {
|
||||
url: tr.banner,
|
||||
column: showTranslations.banner,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
await tx
|
||||
.insert(showTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [showTranslations.pk, showTranslations.language],
|
||||
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
);
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
entryVideoJoin,
|
||||
videos,
|
||||
} from "~/db/schema";
|
||||
import { conflictUpdateAllExcept, values } from "~/db/utils";
|
||||
import { conflictUpdateAllExcept, unnest, unnestValues } from "~/db/utils";
|
||||
import type { SeedEntry as SEntry, SeedExtra as SExtra } from "~/models/entry";
|
||||
import { enqueueOptImage } from "../images";
|
||||
import { record } from "~/otel";
|
||||
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
|
||||
import { guessNextRefresh } from "../refresh";
|
||||
import { updateAvailableCount, updateAvailableSince } from "./shows";
|
||||
|
||||
@@ -42,22 +43,24 @@ const generateSlug = (
|
||||
}
|
||||
};
|
||||
|
||||
export const insertEntries = async (
|
||||
show: { pk: number; slug: string; kind: "movie" | "serie" | "collection" },
|
||||
items: (SeedEntry | SeedExtra)[],
|
||||
onlyExtras = false,
|
||||
) => {
|
||||
if (!items.length) return [];
|
||||
export const insertEntries = record(
|
||||
"insertEntries",
|
||||
async (
|
||||
show: { pk: number; slug: string; kind: "movie" | "serie" | "collection" },
|
||||
items: (SeedEntry | SeedExtra)[],
|
||||
onlyExtras = false,
|
||||
) => {
|
||||
if (!items.length) return [];
|
||||
|
||||
const retEntries = await db.transaction(async (tx) => {
|
||||
const vals: EntryI[] = await Promise.all(
|
||||
items.map(async (seed) => {
|
||||
const retEntries = await db.transaction(async (tx) => {
|
||||
const imgQueue: ImageTask[] = [];
|
||||
const vals: EntryI[] = items.map((seed) => {
|
||||
const { translations, videos, video, ...entry } = seed;
|
||||
return {
|
||||
...entry,
|
||||
showPk: show.pk,
|
||||
slug: generateSlug(show.slug, seed),
|
||||
thumbnail: await enqueueOptImage(tx, {
|
||||
thumbnail: enqueueOptImage(imgQueue, {
|
||||
url: seed.thumbnail,
|
||||
column: entries.thumbnail,
|
||||
}),
|
||||
@@ -72,136 +75,132 @@ export const insertEntries = async (
|
||||
? entry.number
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const ret = await tx
|
||||
.insert(entries)
|
||||
.values(vals)
|
||||
.onConflictDoUpdate({
|
||||
target: entries.slug,
|
||||
set: conflictUpdateAllExcept(entries, [
|
||||
"pk",
|
||||
"showPk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: entries.pk, id: entries.id, slug: entries.slug });
|
||||
|
||||
const trans: EntryTransI[] = (
|
||||
await Promise.all(
|
||||
items.map(async (seed, i) => {
|
||||
if (seed.kind === "extra") {
|
||||
return [
|
||||
{
|
||||
pk: ret[i].pk,
|
||||
// yeah we hardcode the language to extra because if we want to support
|
||||
// translations one day it won't be awkward
|
||||
language: "extra",
|
||||
name: seed.name,
|
||||
description: null,
|
||||
poster: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return await Promise.all(
|
||||
Object.entries(seed.translations).map(async ([lang, tr]) => ({
|
||||
// assumes ret is ordered like items.
|
||||
pk: ret[i].pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
poster:
|
||||
seed.kind === "movie"
|
||||
? await enqueueOptImage(tx, {
|
||||
url: (tr as any).poster,
|
||||
column: entryTranslations.poster,
|
||||
})
|
||||
: undefined,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
await tx
|
||||
.insert(entryTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [entryTranslations.pk, entryTranslations.language],
|
||||
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
|
||||
});
|
||||
const ret = await tx
|
||||
.insert(entries)
|
||||
.select(unnestValues(vals, entries))
|
||||
.onConflictDoUpdate({
|
||||
target: entries.slug,
|
||||
set: conflictUpdateAllExcept(entries, [
|
||||
"pk",
|
||||
"showPk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: entries.pk, id: entries.id, slug: entries.slug });
|
||||
|
||||
return ret;
|
||||
});
|
||||
const trans: EntryTransI[] = items.flatMap((seed, i) => {
|
||||
if (seed.kind === "extra") {
|
||||
return [
|
||||
{
|
||||
pk: ret[i].pk,
|
||||
// yeah we hardcode the language to extra because if we want to support
|
||||
// translations one day it won't be awkward
|
||||
language: "extra",
|
||||
name: seed.name,
|
||||
description: null,
|
||||
poster: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const vids = items.flatMap((seed, i) => {
|
||||
if (seed.kind === "extra") {
|
||||
return {
|
||||
videoId: seed.video,
|
||||
return Object.entries(seed.translations).map(([lang, tr]) => ({
|
||||
// assumes ret is ordered like items.
|
||||
pk: ret[i].pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
poster:
|
||||
seed.kind === "movie"
|
||||
? enqueueOptImage(imgQueue, {
|
||||
url: (tr as any).poster,
|
||||
column: entryTranslations.poster,
|
||||
})
|
||||
: undefined,
|
||||
}));
|
||||
});
|
||||
await flushImageQueue(tx, imgQueue, 0);
|
||||
await tx
|
||||
.insert(entryTranslations)
|
||||
.select(unnestValues(trans, entryTranslations))
|
||||
.onConflictDoUpdate({
|
||||
target: [entryTranslations.pk, entryTranslations.language],
|
||||
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
const vids = items.flatMap((seed, i) => {
|
||||
if (seed.kind === "extra") {
|
||||
return {
|
||||
videoId: seed.video,
|
||||
entryPk: retEntries[i].pk,
|
||||
entrySlug: retEntries[i].slug,
|
||||
needRendering: false,
|
||||
};
|
||||
}
|
||||
if (!seed.videos) return [];
|
||||
return seed.videos.map((x, j) => ({
|
||||
videoId: x,
|
||||
entryPk: retEntries[i].pk,
|
||||
entrySlug: retEntries[i].slug,
|
||||
needRendering: false,
|
||||
};
|
||||
// The first video should not have a rendering.
|
||||
needRendering: j !== 0 && seed.videos!.length > 1,
|
||||
}));
|
||||
});
|
||||
|
||||
if (vids.length === 0) {
|
||||
// we have not added videos but we need to update the `entriesCount`
|
||||
if (show.kind === "serie" && !onlyExtras)
|
||||
await updateAvailableCount(db, [show.pk], true);
|
||||
return retEntries.map((x) => ({ id: x.id, slug: x.slug, videos: [] }));
|
||||
}
|
||||
if (!seed.videos) return [];
|
||||
return seed.videos.map((x, j) => ({
|
||||
videoId: x,
|
||||
entryPk: retEntries[i].pk,
|
||||
entrySlug: retEntries[i].slug,
|
||||
// The first video should not have a rendering.
|
||||
needRendering: j !== 0 && seed.videos!.length > 1,
|
||||
|
||||
const retVideos = await db.transaction(async (tx) => {
|
||||
const ret = await tx
|
||||
.insert(entryVideoJoin)
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
entryPk: sql<number>`vids."entryPk"`.as("entry"),
|
||||
videoPk: videos.pk,
|
||||
slug: computeVideoSlug(
|
||||
sql`vids."entrySlug"`,
|
||||
sql`vids."needRendering"`,
|
||||
),
|
||||
})
|
||||
.from(
|
||||
unnest(vids, "vids", {
|
||||
entryPk: "integer",
|
||||
entrySlug: "varchar(255)",
|
||||
needRendering: "boolean",
|
||||
videoId: "uuid",
|
||||
}),
|
||||
)
|
||||
.innerJoin(videos, eq(videos.id, sql`vids."videoId"`)),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning({
|
||||
slug: entryVideoJoin.slug,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
});
|
||||
|
||||
if (!onlyExtras)
|
||||
await updateAvailableCount(tx, [show.pk], show.kind === "serie");
|
||||
|
||||
await updateAvailableSince(tx, [...new Set(vids.map((x) => x.entryPk))]);
|
||||
return ret;
|
||||
});
|
||||
|
||||
return retEntries.map((entry) => ({
|
||||
id: entry.id,
|
||||
slug: entry.slug,
|
||||
videos: retVideos.filter((x) => x.entryPk === entry.pk),
|
||||
}));
|
||||
});
|
||||
|
||||
if (vids.length === 0) {
|
||||
// we have not added videos but we need to update the `entriesCount`
|
||||
if (show.kind === "serie" && !onlyExtras)
|
||||
await updateAvailableCount(db, [show.pk], true);
|
||||
return retEntries.map((x) => ({ id: x.id, slug: x.slug, videos: [] }));
|
||||
}
|
||||
|
||||
const retVideos = await db.transaction(async (tx) => {
|
||||
const ret = await tx
|
||||
.insert(entryVideoJoin)
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
entryPk: sql<number>`vids.entryPk`.as("entry"),
|
||||
videoPk: videos.pk,
|
||||
slug: computeVideoSlug(
|
||||
sql`vids.entrySlug`,
|
||||
sql`vids.needRendering`,
|
||||
),
|
||||
})
|
||||
.from(
|
||||
values(vids, {
|
||||
entryPk: "integer",
|
||||
needRendering: "boolean",
|
||||
videoId: "uuid",
|
||||
}).as("vids"),
|
||||
)
|
||||
.innerJoin(videos, eq(videos.id, sql`vids.videoId`)),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning({
|
||||
slug: entryVideoJoin.slug,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
});
|
||||
|
||||
if (!onlyExtras)
|
||||
await updateAvailableCount(tx, [show.pk], show.kind === "serie");
|
||||
|
||||
await updateAvailableSince(tx, [...new Set(vids.map((x) => x.entryPk))]);
|
||||
return ret;
|
||||
});
|
||||
|
||||
return retEntries.map((entry) => ({
|
||||
id: entry.id,
|
||||
slug: entry.slug,
|
||||
videos: retVideos.filter((x) => x.entryPk === entry.pk),
|
||||
}));
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export function computeVideoSlug(entrySlug: SQL | Column, needsRendering: SQL) {
|
||||
return sql<string>`
|
||||
|
||||
@@ -1,82 +1,78 @@
|
||||
import { db } from "~/db";
|
||||
import { seasons, seasonTranslations } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import { conflictUpdateAllExcept, unnestValues } from "~/db/utils";
|
||||
import type { SeedSeason } from "~/models/season";
|
||||
import { enqueueOptImage } from "../images";
|
||||
import { record } from "~/otel";
|
||||
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
|
||||
import { guessNextRefresh } from "../refresh";
|
||||
|
||||
type SeasonI = typeof seasons.$inferInsert;
|
||||
type SeasonTransI = typeof seasonTranslations.$inferInsert;
|
||||
|
||||
export const insertSeasons = async (
|
||||
show: { pk: number; slug: string },
|
||||
items: SeedSeason[],
|
||||
) => {
|
||||
if (!items.length) return [];
|
||||
export const insertSeasons = record(
|
||||
"insertSeasons",
|
||||
async (show: { pk: number; slug: string }, items: SeedSeason[]) => {
|
||||
if (!items.length) return [];
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
const vals: SeasonI[] = items.map((x) => {
|
||||
const { translations, ...season } = x;
|
||||
return {
|
||||
...season,
|
||||
showPk: show.pk,
|
||||
slug:
|
||||
season.seasonNumber === 0
|
||||
? `${show.slug}-specials`
|
||||
: `${show.slug}-s${season.seasonNumber}`,
|
||||
nextRefresh: guessNextRefresh(season.startAir ?? new Date()),
|
||||
};
|
||||
});
|
||||
const ret = await tx
|
||||
.insert(seasons)
|
||||
.values(vals)
|
||||
.onConflictDoUpdate({
|
||||
target: seasons.slug,
|
||||
set: conflictUpdateAllExcept(seasons, [
|
||||
"pk",
|
||||
"showPk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: seasons.pk, id: seasons.id, slug: seasons.slug });
|
||||
|
||||
const trans: SeasonTransI[] = (
|
||||
await Promise.all(
|
||||
items.map(
|
||||
async (seed, i) =>
|
||||
await Promise.all(
|
||||
Object.entries(seed.translations).map(async ([lang, tr]) => ({
|
||||
// assumes ret is ordered like items.
|
||||
pk: ret[i].pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
poster: await enqueueOptImage(tx, {
|
||||
url: tr.poster,
|
||||
column: seasonTranslations.poster,
|
||||
}),
|
||||
thumbnail: await enqueueOptImage(tx, {
|
||||
url: tr.thumbnail,
|
||||
column: seasonTranslations.thumbnail,
|
||||
}),
|
||||
banner: await enqueueOptImage(tx, {
|
||||
url: tr.banner,
|
||||
column: seasonTranslations.banner,
|
||||
}),
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
await tx
|
||||
.insert(seasonTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [seasonTranslations.pk, seasonTranslations.language],
|
||||
set: conflictUpdateAllExcept(seasonTranslations, ["pk", "language"]),
|
||||
return db.transaction(async (tx) => {
|
||||
const imgQueue: ImageTask[] = [];
|
||||
const vals: SeasonI[] = items.map((x) => {
|
||||
const { translations, ...season } = x;
|
||||
return {
|
||||
...season,
|
||||
showPk: show.pk,
|
||||
slug:
|
||||
season.seasonNumber === 0
|
||||
? `${show.slug}-specials`
|
||||
: `${show.slug}-s${season.seasonNumber}`,
|
||||
nextRefresh: guessNextRefresh(season.startAir ?? new Date()),
|
||||
};
|
||||
});
|
||||
const ret = await tx
|
||||
.insert(seasons)
|
||||
.select(unnestValues(vals, seasons))
|
||||
.onConflictDoUpdate({
|
||||
target: seasons.slug,
|
||||
set: conflictUpdateAllExcept(seasons, [
|
||||
"pk",
|
||||
"showPk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: seasons.pk, id: seasons.id, slug: seasons.slug });
|
||||
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
const trans: SeasonTransI[] = items.flatMap((seed, i) =>
|
||||
Object.entries(seed.translations).map(([lang, tr]) => ({
|
||||
// assumes ret is ordered like items.
|
||||
pk: ret[i].pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
poster: enqueueOptImage(imgQueue, {
|
||||
url: tr.poster,
|
||||
column: seasonTranslations.poster,
|
||||
}),
|
||||
thumbnail: enqueueOptImage(imgQueue, {
|
||||
url: tr.thumbnail,
|
||||
column: seasonTranslations.thumbnail,
|
||||
}),
|
||||
banner: enqueueOptImage(imgQueue, {
|
||||
url: tr.banner,
|
||||
column: seasonTranslations.banner,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
await flushImageQueue(tx, imgQueue, -10);
|
||||
await tx
|
||||
.insert(seasonTranslations)
|
||||
.select(unnestValues(trans, seasonTranslations))
|
||||
.onConflictDoUpdate({
|
||||
target: [seasonTranslations.pk, seasonTranslations.language],
|
||||
set: conflictUpdateAllExcept(seasonTranslations, ["pk", "language"]),
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -21,86 +21,93 @@ import type { SeedCollection } from "~/models/collections";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
import type { SeedSerie } from "~/models/serie";
|
||||
import type { Original } from "~/models/utils";
|
||||
import { record } from "~/otel";
|
||||
import { getYear } from "~/utils";
|
||||
import { enqueueOptImage } from "../images";
|
||||
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
|
||||
|
||||
type Show = typeof shows.$inferInsert;
|
||||
type ShowTrans = typeof showTranslations.$inferInsert;
|
||||
|
||||
export const insertShow = async (
|
||||
show: Omit<Show, "original">,
|
||||
original: Original & {
|
||||
poster: string | null;
|
||||
thumbnail: string | null;
|
||||
banner: string | null;
|
||||
logo: string | null;
|
||||
},
|
||||
translations:
|
||||
| SeedMovie["translations"]
|
||||
| SeedSerie["translations"]
|
||||
| SeedCollection["translations"],
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const orig = {
|
||||
...original,
|
||||
poster: await enqueueOptImage(tx, {
|
||||
url: original.poster,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['poster']`,
|
||||
}),
|
||||
thumbnail: await enqueueOptImage(tx, {
|
||||
url: original.thumbnail,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['thumbnail']`,
|
||||
}),
|
||||
banner: await enqueueOptImage(tx, {
|
||||
url: original.banner,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['banner']`,
|
||||
}),
|
||||
logo: await enqueueOptImage(tx, {
|
||||
url: original.logo,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['logo']`,
|
||||
}),
|
||||
};
|
||||
const ret = await insertBaseShow(tx, { ...show, original: orig });
|
||||
if ("status" in ret) return ret;
|
||||
export const insertShow = record(
|
||||
"insertShow",
|
||||
async (
|
||||
show: Omit<Show, "original">,
|
||||
original: Original & {
|
||||
poster: string | null;
|
||||
thumbnail: string | null;
|
||||
banner: string | null;
|
||||
logo: string | null;
|
||||
},
|
||||
translations:
|
||||
| SeedMovie["translations"]
|
||||
| SeedSerie["translations"]
|
||||
| SeedCollection["translations"],
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const imgQueue: ImageTask[] = [];
|
||||
const orig = {
|
||||
...original,
|
||||
poster: enqueueOptImage(imgQueue, {
|
||||
url: original.poster,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['poster']`,
|
||||
}),
|
||||
thumbnail: enqueueOptImage(imgQueue, {
|
||||
url: original.thumbnail,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['thumbnail']`,
|
||||
}),
|
||||
banner: enqueueOptImage(imgQueue, {
|
||||
url: original.banner,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['banner']`,
|
||||
}),
|
||||
logo: enqueueOptImage(imgQueue, {
|
||||
url: original.logo,
|
||||
table: shows,
|
||||
column: sql`${shows.original}['logo']`,
|
||||
}),
|
||||
};
|
||||
const ret = await insertBaseShow(tx, { ...show, original: orig });
|
||||
if ("status" in ret) return ret;
|
||||
|
||||
const trans: ShowTrans[] = await Promise.all(
|
||||
Object.entries(translations).map(async ([lang, tr]) => ({
|
||||
pk: ret.pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
latinName: tr.latinName ?? null,
|
||||
poster: await enqueueOptImage(tx, {
|
||||
url: tr.poster,
|
||||
column: showTranslations.poster,
|
||||
const trans: ShowTrans[] = Object.entries(translations).map(
|
||||
([lang, tr]) => ({
|
||||
pk: ret.pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
latinName: tr.latinName ?? null,
|
||||
poster: enqueueOptImage(imgQueue, {
|
||||
url: tr.poster,
|
||||
column: showTranslations.poster,
|
||||
}),
|
||||
thumbnail: enqueueOptImage(imgQueue, {
|
||||
url: tr.thumbnail,
|
||||
column: showTranslations.thumbnail,
|
||||
}),
|
||||
logo: enqueueOptImage(imgQueue, {
|
||||
url: tr.logo,
|
||||
column: showTranslations.logo,
|
||||
}),
|
||||
banner: enqueueOptImage(imgQueue, {
|
||||
url: tr.banner,
|
||||
column: showTranslations.banner,
|
||||
}),
|
||||
}),
|
||||
thumbnail: await enqueueOptImage(tx, {
|
||||
url: tr.thumbnail,
|
||||
column: showTranslations.thumbnail,
|
||||
}),
|
||||
logo: await enqueueOptImage(tx, {
|
||||
url: tr.logo,
|
||||
column: showTranslations.logo,
|
||||
}),
|
||||
banner: await enqueueOptImage(tx, {
|
||||
url: tr.banner,
|
||||
column: showTranslations.banner,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
await tx
|
||||
.insert(showTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [showTranslations.pk, showTranslations.language],
|
||||
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
);
|
||||
await flushImageQueue(tx, imgQueue, 200);
|
||||
// we can't unnest values here because show translations contains arrays.
|
||||
await tx
|
||||
.insert(showTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [showTranslations.pk, showTranslations.language],
|
||||
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
async function insertBaseShow(tx: Transaction, show: Show) {
|
||||
function insert() {
|
||||
|
||||
@@ -1,58 +1,63 @@
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import { roles, staff } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import { conflictUpdateAllExcept, unnestValues } from "~/db/utils";
|
||||
import type { SeedStaff } from "~/models/staff";
|
||||
import { enqueueOptImage } from "../images";
|
||||
import { record } from "~/otel";
|
||||
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
|
||||
|
||||
export const insertStaff = async (
|
||||
seed: SeedStaff[] | undefined,
|
||||
showPk: number,
|
||||
) => {
|
||||
if (!seed?.length) return [];
|
||||
export const insertStaff = record(
|
||||
"insertStaff",
|
||||
async (seed: SeedStaff[] | undefined, showPk: number) => {
|
||||
if (!seed?.length) return [];
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const people = await Promise.all(
|
||||
seed.map(async (x) => ({
|
||||
return await db.transaction(async (tx) => {
|
||||
const imgQueue: ImageTask[] = [];
|
||||
const people = seed.map((x) => ({
|
||||
...x.staff,
|
||||
image: await enqueueOptImage(tx, {
|
||||
image: enqueueOptImage(imgQueue, {
|
||||
url: x.staff.image,
|
||||
column: staff.image,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
const ret = await tx
|
||||
.insert(staff)
|
||||
.values(people)
|
||||
.onConflictDoUpdate({
|
||||
target: staff.slug,
|
||||
set: conflictUpdateAllExcept(staff, ["pk", "id", "slug", "createdAt"]),
|
||||
})
|
||||
.returning({ pk: staff.pk, id: staff.id, slug: staff.slug });
|
||||
}));
|
||||
const ret = await tx
|
||||
.insert(staff)
|
||||
.select(unnestValues(people, staff))
|
||||
.onConflictDoUpdate({
|
||||
target: staff.slug,
|
||||
set: conflictUpdateAllExcept(staff, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: staff.pk, id: staff.id, slug: staff.slug });
|
||||
|
||||
const rval = await Promise.all(
|
||||
seed.map(async (x, i) => ({
|
||||
const rval = seed.map((x, i) => ({
|
||||
showPk,
|
||||
staffPk: ret[i].pk,
|
||||
kind: x.kind,
|
||||
order: i,
|
||||
character: {
|
||||
...x.character,
|
||||
image: await enqueueOptImage(tx, {
|
||||
image: enqueueOptImage(imgQueue, {
|
||||
url: x.character.image,
|
||||
table: roles,
|
||||
column: sql`${roles.character}['image']`,
|
||||
}),
|
||||
},
|
||||
})),
|
||||
);
|
||||
}));
|
||||
|
||||
// always replace all roles. this is because:
|
||||
// - we want `order` to stay in sync (& without duplicates)
|
||||
// - we don't have ways to identify a role so we can't onConflict
|
||||
await tx.delete(roles).where(eq(roles.showPk, showPk));
|
||||
await tx.insert(roles).values(rval);
|
||||
await flushImageQueue(tx, imgQueue, -200);
|
||||
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
// always replace all roles. this is because:
|
||||
// - we want `order` to stay in sync (& without duplicates)
|
||||
// - we don't have ways to identify a role so we can't onConflict
|
||||
await tx.delete(roles).where(eq(roles.showPk, showPk));
|
||||
await tx.insert(roles).select(unnestValues(rval, roles));
|
||||
|
||||
return ret;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,68 +1,74 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import { showStudioJoin, studios, studioTranslations } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import { conflictUpdateAllExcept, sqlarr, unnestValues } from "~/db/utils";
|
||||
import type { SeedStudio } from "~/models/studio";
|
||||
import { enqueueOptImage } from "../images";
|
||||
import { record } from "~/otel";
|
||||
import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images";
|
||||
|
||||
type StudioI = typeof studios.$inferInsert;
|
||||
type StudioTransI = typeof studioTranslations.$inferInsert;
|
||||
|
||||
export const insertStudios = async (
|
||||
seed: SeedStudio[] | undefined,
|
||||
showPk: number,
|
||||
) => {
|
||||
if (!seed?.length) return [];
|
||||
export const insertStudios = record(
|
||||
"insertStudios",
|
||||
async (seed: SeedStudio[] | undefined, showPk: number) => {
|
||||
if (!seed?.length) return [];
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const vals: StudioI[] = seed.map((x) => {
|
||||
const { translations, ...item } = x;
|
||||
return item;
|
||||
});
|
||||
|
||||
const ret = await tx
|
||||
.insert(studios)
|
||||
.values(vals)
|
||||
.onConflictDoUpdate({
|
||||
target: studios.slug,
|
||||
set: conflictUpdateAllExcept(studios, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: studios.pk, id: studios.id, slug: studios.slug });
|
||||
|
||||
const trans: StudioTransI[] = (
|
||||
await Promise.all(
|
||||
seed.map(
|
||||
async (x, i) =>
|
||||
await Promise.all(
|
||||
Object.entries(x.translations).map(async ([lang, tr]) => ({
|
||||
pk: ret[i].pk,
|
||||
language: lang,
|
||||
name: tr.name,
|
||||
logo: await enqueueOptImage(tx, {
|
||||
url: tr.logo,
|
||||
column: studioTranslations.logo,
|
||||
}),
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
await tx
|
||||
.insert(studioTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [studioTranslations.pk, studioTranslations.language],
|
||||
set: conflictUpdateAllExcept(studioTranslations, ["pk", "language"]),
|
||||
return await db.transaction(async (tx) => {
|
||||
const vals: StudioI[] = seed.map((x) => {
|
||||
const { translations, ...item } = x;
|
||||
return item;
|
||||
});
|
||||
|
||||
await tx
|
||||
.insert(showStudioJoin)
|
||||
.values(ret.map((studio) => ({ showPk: showPk, studioPk: studio.pk })))
|
||||
.onConflictDoNothing();
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
const ret = await tx
|
||||
.insert(studios)
|
||||
.select(unnestValues(vals, studios))
|
||||
.onConflictDoUpdate({
|
||||
target: studios.slug,
|
||||
set: conflictUpdateAllExcept(studios, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: studios.pk, id: studios.id, slug: studios.slug });
|
||||
|
||||
const imgQueue: ImageTask[] = [];
|
||||
const trans: StudioTransI[] = seed.flatMap((x, i) =>
|
||||
Object.entries(x.translations).map(([lang, tr]) => ({
|
||||
pk: ret[i].pk,
|
||||
language: lang,
|
||||
name: tr.name,
|
||||
logo: enqueueOptImage(imgQueue, {
|
||||
url: tr.logo,
|
||||
column: studioTranslations.logo,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
await flushImageQueue(tx, imgQueue, -100);
|
||||
await tx
|
||||
.insert(studioTranslations)
|
||||
.select(unnestValues(trans, studioTranslations))
|
||||
.onConflictDoUpdate({
|
||||
target: [studioTranslations.pk, studioTranslations.language],
|
||||
set: conflictUpdateAllExcept(studioTranslations, ["pk", "language"]),
|
||||
});
|
||||
|
||||
await tx
|
||||
.insert(showStudioJoin)
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
showPk: sql`${showPk}`.as("showPk"),
|
||||
studioPk: sql`v."studioPk"`.as("studioPk"),
|
||||
})
|
||||
.from(
|
||||
sql`unnest(${sqlarr(ret.map((x) => x.pk))}::integer[]) as v("studioPk")`,
|
||||
),
|
||||
)
|
||||
.onConflictDoNothing();
|
||||
return ret;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -143,7 +143,7 @@ export const collections = new Elysia({
|
||||
"",
|
||||
async ({
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
jwt: { sub, settings },
|
||||
request: { url },
|
||||
}) => {
|
||||
@@ -158,7 +158,7 @@ export const collections = new Elysia({
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all collections" },
|
||||
@@ -227,7 +227,7 @@ export const collections = new Elysia({
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
jwt: { sub, settings },
|
||||
request: { url },
|
||||
status,
|
||||
@@ -265,7 +265,7 @@ export const collections = new Elysia({
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all movies in a collection" },
|
||||
@@ -284,7 +284,7 @@ export const collections = new Elysia({
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
jwt: { sub, settings },
|
||||
request: { url },
|
||||
status,
|
||||
@@ -322,7 +322,7 @@ export const collections = new Elysia({
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all series in a collection" },
|
||||
@@ -341,7 +341,7 @@ export const collections = new Elysia({
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
jwt: { sub, settings },
|
||||
request: { url },
|
||||
status,
|
||||
@@ -375,7 +375,7 @@ export const collections = new Elysia({
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all series & movies in a collection" },
|
||||
|
||||
@@ -133,7 +133,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||
"",
|
||||
async ({
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub, settings },
|
||||
}) => {
|
||||
@@ -148,7 +148,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all movies" },
|
||||
|
||||
@@ -136,7 +136,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
||||
"",
|
||||
async ({
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub, settings },
|
||||
}) => {
|
||||
@@ -151,7 +151,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all series" },
|
||||
|
||||
@@ -63,7 +63,7 @@ export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] })
|
||||
preferOriginal,
|
||||
ignoreInCollection,
|
||||
},
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub, settings },
|
||||
}) => {
|
||||
@@ -81,7 +81,7 @@ export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] })
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all movies/series/collections" },
|
||||
|
||||
@@ -189,7 +189,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
jwt: { sub, settings },
|
||||
status,
|
||||
@@ -227,7 +227,6 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
.from(watchlist)
|
||||
.leftJoin(profiles, eq(watchlist.profilePk, profiles.pk))
|
||||
.where(and(eq(profiles.id, sub), eq(watchlist.showPk, shows.pk)))
|
||||
.limit(1)
|
||||
.as("watchstatus");
|
||||
|
||||
const items = await db
|
||||
@@ -270,7 +269,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
roles.showPk,
|
||||
)
|
||||
.limit(limit);
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
@@ -317,7 +316,11 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
)
|
||||
.get(
|
||||
"/staff",
|
||||
async ({ query: { limit, after, sort, query }, request: { url } }) => {
|
||||
async ({
|
||||
query: { limit, after, sort, query },
|
||||
request: { url },
|
||||
headers,
|
||||
}) => {
|
||||
const items = await db
|
||||
.select()
|
||||
.from(staff)
|
||||
@@ -334,7 +337,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
staff.pk,
|
||||
)
|
||||
.limit(limit);
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
@@ -363,6 +366,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter },
|
||||
request: { url },
|
||||
headers,
|
||||
status,
|
||||
}) => {
|
||||
const [movie] = await db
|
||||
@@ -390,7 +394,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
sort,
|
||||
filter: and(eq(roles.showPk, movie.pk), filter),
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
@@ -430,6 +434,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter },
|
||||
request: { url },
|
||||
headers,
|
||||
status,
|
||||
}) => {
|
||||
const [serie] = await db
|
||||
@@ -457,7 +462,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
||||
sort,
|
||||
filter: and(eq(roles.showPk, serie.pk), filter),
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
|
||||
@@ -228,7 +228,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
"",
|
||||
async ({
|
||||
query: { limit, after, query, sort },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
request: { url },
|
||||
}) => {
|
||||
const langs = processLanguages(languages);
|
||||
@@ -239,7 +239,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
sort,
|
||||
languages: langs,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all studios" },
|
||||
@@ -302,7 +302,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
jwt: { sub, settings },
|
||||
request: { url },
|
||||
status,
|
||||
@@ -344,7 +344,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all series & movies made by a studio." },
|
||||
@@ -363,7 +363,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
jwt: { sub, settings },
|
||||
request: { url },
|
||||
status,
|
||||
@@ -406,7 +406,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all movies made by a studio." },
|
||||
@@ -425,7 +425,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
async ({
|
||||
params: { id },
|
||||
query: { limit, after, query, sort, filter, preferOriginal },
|
||||
headers: { "accept-language": languages },
|
||||
headers: { "accept-language": languages, ...headers },
|
||||
jwt: { sub, settings },
|
||||
request: { url },
|
||||
status,
|
||||
@@ -468,7 +468,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
userId: sub,
|
||||
});
|
||||
return createPage(items, { url, sort, limit });
|
||||
return createPage(items, { url, sort, limit, headers });
|
||||
},
|
||||
{
|
||||
detail: { description: "Get all series made by a studio." },
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
lt,
|
||||
max,
|
||||
min,
|
||||
ne,
|
||||
notExists,
|
||||
or,
|
||||
sql,
|
||||
@@ -15,7 +16,16 @@ import { alias } from "drizzle-orm/pg-core";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { auth } from "~/auth";
|
||||
import { db, type Transaction } from "~/db";
|
||||
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||
import {
|
||||
entries,
|
||||
entryVideoJoin,
|
||||
history,
|
||||
profiles,
|
||||
shows,
|
||||
showTranslations,
|
||||
videos,
|
||||
} from "~/db/schema";
|
||||
import { watchlist } from "~/db/schema/watchlist";
|
||||
import {
|
||||
coalesce,
|
||||
conflictUpdateAllExcept,
|
||||
@@ -25,15 +35,20 @@ import {
|
||||
jsonbBuildObject,
|
||||
jsonbObjectAgg,
|
||||
sqlarr,
|
||||
values,
|
||||
unnest,
|
||||
unnestValues,
|
||||
} from "~/db/utils";
|
||||
import { Entry } from "~/models/entry";
|
||||
import { KError } from "~/models/error";
|
||||
import { bubbleVideo } from "~/models/examples";
|
||||
import { Progress } from "~/models/history";
|
||||
import { Movie, type MovieStatus } from "~/models/movie";
|
||||
import { Serie } from "~/models/serie";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
buildRelations,
|
||||
createPage,
|
||||
type Image,
|
||||
isUuid,
|
||||
keysetPaginate,
|
||||
Page,
|
||||
@@ -44,6 +59,7 @@ import {
|
||||
} from "~/models/utils";
|
||||
import { desc as description } from "~/models/utils/descriptions";
|
||||
import { Guess, Guesses, SeedVideo, Video } from "~/models/video";
|
||||
import type { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist";
|
||||
import { comment } from "~/utils";
|
||||
import {
|
||||
entryProgressQ,
|
||||
@@ -86,10 +102,23 @@ async function linkVideos(
|
||||
.innerJoin(shows, eq(entries.showPk, shows.pk))
|
||||
.as("entriesQ");
|
||||
|
||||
const hasRenderingQ = tx
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entryVideoJoin.entryPk, entriesQ.pk));
|
||||
const renderVid = alias(videos, "renderVid");
|
||||
const hasRenderingQ = or(
|
||||
gt(
|
||||
sql`dense_rank() over (partition by ${entriesQ.pk} order by ${videos.rendering})`,
|
||||
1,
|
||||
),
|
||||
sql`exists(${tx
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.innerJoin(renderVid, eq(renderVid.pk, entryVideoJoin.videoPk))
|
||||
.where(
|
||||
and(
|
||||
eq(entryVideoJoin.entryPk, entriesQ.pk),
|
||||
ne(renderVid.rendering, videos.rendering),
|
||||
),
|
||||
)})`,
|
||||
)!;
|
||||
|
||||
const ret = await tx
|
||||
.insert(entryVideoJoin)
|
||||
@@ -98,13 +127,13 @@ async function linkVideos(
|
||||
.selectDistinctOn([entriesQ.pk, videos.pk], {
|
||||
entryPk: entriesQ.pk,
|
||||
videoPk: videos.pk,
|
||||
slug: computeVideoSlug(entriesQ.slug, sql`exists(${hasRenderingQ})`),
|
||||
slug: computeVideoSlug(entriesQ.slug, hasRenderingQ),
|
||||
})
|
||||
.from(
|
||||
values(links, {
|
||||
unnest(links, "j", {
|
||||
video: "integer",
|
||||
entry: "jsonb",
|
||||
}).as("j"),
|
||||
}),
|
||||
)
|
||||
.innerJoin(videos, eq(videos.pk, sql`j.video`))
|
||||
.innerJoin(
|
||||
@@ -206,14 +235,44 @@ const videoRelations = {
|
||||
slugs: () => {
|
||||
return db
|
||||
.select({
|
||||
slugs: coalesce(jsonbAgg(entryVideoJoin.slug), sql`'[]'::jsonb`).as(
|
||||
"slugs",
|
||||
),
|
||||
slugs: coalesce<string[]>(
|
||||
jsonbAgg(entryVideoJoin.slug),
|
||||
sql`'[]'::jsonb`,
|
||||
).as("slugs"),
|
||||
})
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entryVideoJoin.videoPk, videos.pk))
|
||||
.as("slugs");
|
||||
},
|
||||
progress: () => {
|
||||
const query = db
|
||||
.select({
|
||||
json: jsonbBuildObject<Progress>({
|
||||
percent: history.percent,
|
||||
time: history.time,
|
||||
playedDate: sql`to_char(${history.playedDate}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
videoId: videos.id,
|
||||
}),
|
||||
})
|
||||
.from(history)
|
||||
.innerJoin(profiles, eq(history.profilePk, profiles.pk))
|
||||
.where(
|
||||
and(
|
||||
eq(profiles.id, sql.placeholder("userId")),
|
||||
eq(history.videoPk, videos.pk),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(history.playedDate))
|
||||
.limit(1);
|
||||
return sql`
|
||||
(
|
||||
select coalesce(
|
||||
${query},
|
||||
'{"percent": 0, "time": 0, "playedDate": null, "videoId": null}'::jsonb
|
||||
)
|
||||
as "progress"
|
||||
)` as any;
|
||||
},
|
||||
entries: ({ languages }: { languages: string[] }) => {
|
||||
const transQ = getEntryTransQ(languages);
|
||||
|
||||
@@ -229,6 +288,7 @@ const videoRelations = {
|
||||
progress: mapProgress({ aliased: false }),
|
||||
createdAt: sql`to_char(${entries.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
updatedAt: sql`to_char(${entries.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
availableSince: sql`to_char(${entries.availableSince}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
}),
|
||||
),
|
||||
sql`'[]'::jsonb`,
|
||||
@@ -242,6 +302,74 @@ const videoRelations = {
|
||||
.where(eq(entryVideoJoin.videoPk, videos.pk))
|
||||
.as("entries");
|
||||
},
|
||||
show: ({
|
||||
languages,
|
||||
preferOriginal,
|
||||
}: {
|
||||
languages: string[];
|
||||
preferOriginal: boolean;
|
||||
}) => {
|
||||
const transQ = db
|
||||
.selectDistinctOn([showTranslations.pk])
|
||||
.from(showTranslations)
|
||||
.orderBy(
|
||||
showTranslations.pk,
|
||||
sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
|
||||
)
|
||||
.as("t");
|
||||
|
||||
const watchStatusQ = db
|
||||
.select({
|
||||
watchStatus: jsonbBuildObject<MovieWatchStatus & SerieWatchStatus>({
|
||||
...getColumns(watchlist),
|
||||
percent: watchlist.seenCount,
|
||||
}).as("watchStatus"),
|
||||
})
|
||||
.from(watchlist)
|
||||
.leftJoin(profiles, eq(watchlist.profilePk, profiles.pk))
|
||||
.where(
|
||||
and(
|
||||
eq(profiles.id, sql.placeholder("userId")),
|
||||
eq(watchlist.showPk, shows.pk),
|
||||
),
|
||||
);
|
||||
|
||||
return db
|
||||
.select({
|
||||
json: jsonbBuildObject<Serie | Movie>({
|
||||
...getColumns(shows),
|
||||
...getColumns(transQ),
|
||||
// movie columns (status is only a typescript hint)
|
||||
status: sql<MovieStatus>`${shows.status}`,
|
||||
airDate: shows.startAir,
|
||||
kind: sql<any>`${shows.kind}`,
|
||||
isAvailable: sql<boolean>`${shows.availableCount} != 0`,
|
||||
createdAt: sql`to_char(${shows.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
updatedAt: sql`to_char(${shows.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
|
||||
...(preferOriginal && {
|
||||
poster: sql<Image>`coalesce(nullif(${shows.original}->'poster', 'null'::jsonb), ${transQ.poster})`,
|
||||
thumbnail: sql<Image>`coalesce(nullif(${shows.original}->'thumbnail', 'null'::jsonb), ${transQ.thumbnail})`,
|
||||
banner: sql<Image>`coalesce(nullif(${shows.original}->'banner', 'null'::jsonb), ${transQ.banner})`,
|
||||
logo: sql<Image>`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`,
|
||||
}),
|
||||
watchStatus: sql`${watchStatusQ}`,
|
||||
}).as("json"),
|
||||
})
|
||||
.from(shows)
|
||||
.innerJoin(transQ, eq(shows.pk, transQ.pk))
|
||||
.where(
|
||||
eq(
|
||||
shows.pk,
|
||||
db
|
||||
.select({ pk: entries.showPk })
|
||||
.from(entries)
|
||||
.innerJoin(entryVideoJoin, eq(entryVideoJoin.entryPk, entries.pk))
|
||||
.where(eq(videos.pk, entryVideoJoin.videoPk)),
|
||||
),
|
||||
)
|
||||
.as("show");
|
||||
},
|
||||
previous: ({ languages }: { languages: string[] }) => {
|
||||
return getNextVideoEntry({ languages, prev: true });
|
||||
},
|
||||
@@ -263,7 +391,7 @@ function getNextVideoEntry({
|
||||
const evj = alias(entryVideoJoin, `evj_${prev ? "prev" : "next"}`);
|
||||
return db
|
||||
.select({
|
||||
json: jsonbBuildObject<Entry>({
|
||||
json: jsonbBuildObject<{ video: string; entry: Entry }>({
|
||||
video: entryVideoJoin.slug,
|
||||
entry: {
|
||||
...getColumns(entries),
|
||||
@@ -274,7 +402,7 @@ function getNextVideoEntry({
|
||||
createdAt: sql`to_char(${entries.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
updatedAt: sql`to_char(${entries.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||
},
|
||||
}),
|
||||
}).as("json"),
|
||||
})
|
||||
.from(entries)
|
||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||
@@ -326,10 +454,9 @@ function getNextVideoEntry({
|
||||
.as("next");
|
||||
}
|
||||
|
||||
export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
export const videosReadH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
.model({
|
||||
video: Video,
|
||||
"created-videos": t.Array(CreatedVideo),
|
||||
error: t.Object({}),
|
||||
})
|
||||
.use(auth)
|
||||
@@ -337,9 +464,9 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
":id",
|
||||
async ({
|
||||
params: { id },
|
||||
query: { with: relations },
|
||||
query: { with: relations, preferOriginal },
|
||||
headers: { "accept-language": langs },
|
||||
jwt: { sub },
|
||||
jwt: { sub, settings },
|
||||
status,
|
||||
}) => {
|
||||
const languages = processLanguages(langs);
|
||||
@@ -351,10 +478,11 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
.select({
|
||||
...getColumns(videos),
|
||||
...buildRelations(
|
||||
["slugs", "entries", ...relations],
|
||||
["slugs", "progress", "entries", ...relations],
|
||||
videoRelations,
|
||||
{
|
||||
languages,
|
||||
preferOriginal: preferOriginal ?? settings.preferOriginal,
|
||||
},
|
||||
),
|
||||
})
|
||||
@@ -369,7 +497,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
message: `No video found with id or slug '${id}'`,
|
||||
});
|
||||
}
|
||||
return video;
|
||||
return video as any;
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
@@ -382,10 +510,15 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
}),
|
||||
}),
|
||||
query: t.Object({
|
||||
with: t.Array(t.UnionEnum(["previous", "next"]), {
|
||||
with: t.Array(t.UnionEnum(["previous", "next", "show"]), {
|
||||
default: [],
|
||||
description: "Include related entries in the response.",
|
||||
}),
|
||||
preferOriginal: t.Optional(
|
||||
t.Boolean({
|
||||
description: description.preferOriginal,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
headers: t.Object(
|
||||
{
|
||||
@@ -400,6 +533,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
slugs: t.Array(
|
||||
t.String({ format: "slug", examples: ["made-in-abyss-s1e13"] }),
|
||||
),
|
||||
progress: Progress,
|
||||
entries: t.Array(Entry),
|
||||
previous: t.Optional(
|
||||
t.Nullable(
|
||||
@@ -423,6 +557,12 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
}),
|
||||
),
|
||||
),
|
||||
show: t.Optional(
|
||||
t.Union([
|
||||
t.Composite([t.Object({ kind: t.Literal("movie") }), Movie]),
|
||||
t.Composite([t.Object({ kind: t.Literal("serie") }), Serie]),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
]),
|
||||
404: {
|
||||
@@ -433,6 +573,133 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
},
|
||||
},
|
||||
)
|
||||
.get(
|
||||
":id/info",
|
||||
async ({ params: { id }, status, redirect }) => {
|
||||
const [video] = await db
|
||||
.select({
|
||||
path: videos.path,
|
||||
})
|
||||
.from(videos)
|
||||
.leftJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.where(isUuid(id) ? eq(videos.id, id) : eq(entryVideoJoin.slug, id))
|
||||
.limit(1);
|
||||
|
||||
if (!video) {
|
||||
return status(404, {
|
||||
status: 404,
|
||||
message: `No video found with id or slug '${id}'`,
|
||||
});
|
||||
}
|
||||
const path = Buffer.from(video.path, "utf8").toString("base64url");
|
||||
return redirect(`/video/${path}/info`);
|
||||
},
|
||||
{
|
||||
detail: { description: "Get a video's metadata informations" },
|
||||
params: t.Object({
|
||||
id: t.String({
|
||||
description: "The id or slug of the video to retrieve.",
|
||||
example: "made-in-abyss-s1e13",
|
||||
}),
|
||||
}),
|
||||
response: {
|
||||
302: t.Void({
|
||||
description:
|
||||
"Redirected to the [/video/{path}/info](?api=transcoder#tag/metadata/get/:path/info) route (of the transcoder)",
|
||||
}),
|
||||
404: {
|
||||
...KError,
|
||||
description: "No video found with the given id or slug.",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
.get(
|
||||
":id/direct",
|
||||
async ({ params: { id }, status, redirect }) => {
|
||||
const [video] = await db
|
||||
.select({
|
||||
path: videos.path,
|
||||
})
|
||||
.from(videos)
|
||||
.leftJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.where(isUuid(id) ? eq(videos.id, id) : eq(entryVideoJoin.slug, id))
|
||||
.limit(1);
|
||||
|
||||
if (!video) {
|
||||
return status(404, {
|
||||
status: 404,
|
||||
message: `No video found with id or slug '${id}'`,
|
||||
});
|
||||
}
|
||||
const path = Buffer.from(video.path, "utf8").toString("base64url");
|
||||
const filename = path.substring(path.lastIndexOf("/") + 1);
|
||||
return redirect(`/video/${path}/direct/${filename}`);
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
description: "Get redirected to the direct stream of the video",
|
||||
},
|
||||
params: t.Object({
|
||||
id: t.String({
|
||||
description: "The id or slug of the video to watch.",
|
||||
example: "made-in-abyss-s1e13",
|
||||
}),
|
||||
}),
|
||||
response: {
|
||||
302: t.Void({
|
||||
description:
|
||||
"Redirected to the [/video/{path}/direct](?api=transcoder#tag/metadata/get/:path/direct) route (of the transcoder)",
|
||||
}),
|
||||
404: {
|
||||
...KError,
|
||||
description: "No video found with the given id or slug.",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
.get(
|
||||
":id/master.m3u8",
|
||||
async ({ params: { id }, request, status, redirect }) => {
|
||||
const [video] = await db
|
||||
.select({
|
||||
path: videos.path,
|
||||
})
|
||||
.from(videos)
|
||||
.leftJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.where(isUuid(id) ? eq(videos.id, id) : eq(entryVideoJoin.slug, id))
|
||||
.limit(1);
|
||||
|
||||
if (!video) {
|
||||
return status(404, {
|
||||
status: 404,
|
||||
message: `No video found with id or slug '${id}'`,
|
||||
});
|
||||
}
|
||||
const path = Buffer.from(video.path, "utf8").toString("base64url");
|
||||
const query = request.url.substring(request.url.indexOf("?"));
|
||||
return redirect(`/video/${path}/master.m3u8${query}`);
|
||||
},
|
||||
{
|
||||
detail: { description: "Get redirected to the master.m3u8 of the video" },
|
||||
params: t.Object({
|
||||
id: t.String({
|
||||
description: "The id or slug of the video to watch.",
|
||||
example: "made-in-abyss-s1e13",
|
||||
}),
|
||||
}),
|
||||
response: {
|
||||
302: t.Void({
|
||||
description:
|
||||
"Redirected to the [/video/{path}/master.m3u8](?api=transcoder#tag/metadata/get/:path/master.m3u8) route (of the transcoder)",
|
||||
}),
|
||||
404: {
|
||||
...KError,
|
||||
description: "No video found with the given id or slug.",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"",
|
||||
async () => {
|
||||
@@ -552,7 +819,15 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
422: KError,
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
export const videosWriteH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
.model({
|
||||
video: Video,
|
||||
"created-videos": t.Array(CreatedVideo),
|
||||
error: t.Object({}),
|
||||
})
|
||||
.use(auth)
|
||||
.post(
|
||||
"",
|
||||
async ({ body, status }) => {
|
||||
@@ -561,7 +836,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
try {
|
||||
vids = await tx
|
||||
.insert(videos)
|
||||
.values(body)
|
||||
.select(unnestValues(body, videos))
|
||||
.onConflictDoUpdate({
|
||||
target: [videos.path],
|
||||
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import tls, { type ConnectionOptions } from "node:tls";
|
||||
import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { migrate as migrateDb } from "drizzle-orm/node-postgres/migrator";
|
||||
import type { PoolConfig } from "pg";
|
||||
import { record } from "~/otel";
|
||||
import * as schema from "./schema";
|
||||
|
||||
async function getPostgresConfig(): Promise<PoolConfig> {
|
||||
const config: PoolConfig = {
|
||||
connectionString: process.env.POSTGRES_URL,
|
||||
host: process.env.PGHOST ?? "postgres",
|
||||
port: Number(process.env.PGPORT) || 5432,
|
||||
database: process.env.PGDATABASE ?? "kyoo",
|
||||
user: process.env.PGUSER ?? "kyoo",
|
||||
password: process.env.PGPASSWORD ?? "password",
|
||||
options: process.env.PGOPTIONS,
|
||||
application_name: process.env.PGAPPNAME ?? "kyoo",
|
||||
};
|
||||
const config: PoolConfig = {
|
||||
connectionString: process.env.POSTGRES_URL,
|
||||
host: process.env.PGHOST ?? "postgres",
|
||||
port: Number(process.env.PGPORT) || 5432,
|
||||
database: process.env.PGDATABASE ?? "kyoo",
|
||||
user: process.env.PGUSER ?? "kyoo",
|
||||
password: process.env.PGPASSWORD ?? "password",
|
||||
options: process.env.PGOPTIONS,
|
||||
application_name: process.env.PGAPPNAME ?? "kyoo",
|
||||
};
|
||||
|
||||
async function parseSslConfig(): Promise<PoolConfig> {
|
||||
// Due to an upstream bug, if `ssl` is not falsey, an SSL connection will always be attempted. This means
|
||||
// that non-SSL connection options under `ssl` (which is incorrectly named) cannot be set unless SSL is enabled.
|
||||
if (!process.env.PGSSLMODE || process.env.PGSSLMODE === "disable")
|
||||
if (!process.env.PGSSLMODE || process.env.PGSSLMODE === "disable") {
|
||||
config.ssl = false;
|
||||
return config;
|
||||
}
|
||||
|
||||
// Despite this field's name, it is used to configure everything below the application layer.
|
||||
const ssl: ConnectionOptions = {};
|
||||
@@ -107,28 +111,52 @@ async function getPostgresConfig(): Promise<PoolConfig> {
|
||||
return config;
|
||||
}
|
||||
|
||||
const postgresConfig = await getPostgresConfig();
|
||||
const postgresConfig = await parseSslConfig();
|
||||
// use this when using drizzle-kit since it can't parse await statements
|
||||
// const postgresConfig = config;
|
||||
|
||||
console.log("Connecting to postgres with config", {
|
||||
...postgresConfig,
|
||||
password: postgresConfig.password ? "<redacted>" : undefined,
|
||||
ssl:
|
||||
postgresConfig.ssl && typeof postgresConfig.ssl === "object"
|
||||
? {
|
||||
...postgresConfig.ssl,
|
||||
key: "<redacted>",
|
||||
cert: "<redacted>",
|
||||
ca: "<redacted>",
|
||||
}
|
||||
: postgresConfig.ssl,
|
||||
});
|
||||
export const db = drizzle({
|
||||
schema,
|
||||
connection: postgresConfig,
|
||||
casing: "snake_case",
|
||||
});
|
||||
instrumentDrizzleClient(db, {
|
||||
maxQueryTextLength: 100_000_000,
|
||||
});
|
||||
|
||||
export const migrate = async () => {
|
||||
await db.execute(
|
||||
sql.raw(`
|
||||
create extension if not exists pg_trgm;
|
||||
SET pg_trgm.word_similarity_threshold = 0.4;
|
||||
ALTER DATABASE "${postgresConfig.database}" SET pg_trgm.word_similarity_threshold = 0.4;
|
||||
`),
|
||||
);
|
||||
export const migrate = record("migrate", async () => {
|
||||
const APP_SCHEMA = "kyoo";
|
||||
try {
|
||||
await db.execute(
|
||||
sql.raw(`
|
||||
create schema if not exists ${APP_SCHEMA};
|
||||
create extension if not exists pg_trgm schema ${APP_SCHEMA};
|
||||
set pg_trgm.word_similarity_threshold = 0.4;
|
||||
alter database "${postgresConfig.database}" set pg_trgm.word_similarity_threshold = 0.4;
|
||||
`),
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error("Error while updating pg_trgm", err.message);
|
||||
}
|
||||
await migrateDb(db, {
|
||||
migrationsSchema: "kyoo",
|
||||
migrationsSchema: APP_SCHEMA,
|
||||
migrationsFolder: "./drizzle",
|
||||
});
|
||||
console.log(`Database ${postgresConfig.database} migrated!`);
|
||||
};
|
||||
});
|
||||
|
||||
export type Transaction =
|
||||
| typeof db
|
||||
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { image, language, schema } from "./utils";
|
||||
import { image, language, schema, timestamp } from "./utils";
|
||||
import { entryVideoJoin } from "./videos";
|
||||
|
||||
export const entryType = schema.enum("entry_type", [
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, index, integer } from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { profiles } from "./profiles";
|
||||
import { schema } from "./utils";
|
||||
import { schema, timestamp } from "./utils";
|
||||
import { videos } from "./videos";
|
||||
|
||||
export const history = schema.table(
|
||||
@@ -18,7 +17,7 @@ export const history = schema.table(
|
||||
.references(() => entries.pk, { onDelete: "cascade" }),
|
||||
videoPk: integer().references(() => videos.pk, { onDelete: "set null" }),
|
||||
percent: integer().notNull().default(0),
|
||||
time: integer(),
|
||||
time: integer().notNull().default(0),
|
||||
playedDate: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
.default(sql`now()`),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { index, integer, jsonb, uuid, varchar } from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { schema } from "./utils";
|
||||
import { schema, timestamp } from "./utils";
|
||||
|
||||
export const mqueue = schema.table(
|
||||
"mqueue",
|
||||
@@ -9,6 +8,7 @@ export const mqueue = schema.table(
|
||||
id: uuid().notNull().primaryKey().defaultRandom(),
|
||||
kind: varchar({ length: 255 }).notNull(),
|
||||
message: jsonb().notNull(),
|
||||
priority: integer().notNull().default(0),
|
||||
attempt: integer().notNull().default(0),
|
||||
createdAt: timestamp({ withTimezone: true, mode: "iso" })
|
||||
.notNull()
|
||||
|
||||
@@ -10,9 +10,8 @@ import {
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { image, language, schema } from "./utils";
|
||||
import { image, language, schema, timestamp } from "./utils";
|
||||
|
||||
export const season_extid = () =>
|
||||
jsonb()
|
||||
@@ -40,7 +39,7 @@ export const seasons = schema.table(
|
||||
startAir: date(),
|
||||
endAir: date(),
|
||||
|
||||
entriesCount: integer().notNull(),
|
||||
entriesCount: integer().notNull().default(0),
|
||||
availableCount: integer().notNull().default(0),
|
||||
|
||||
externalId: season_extid(),
|
||||
|
||||
@@ -13,12 +13,11 @@ import {
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { Image, Original } from "~/models/utils";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { seasons } from "./seasons";
|
||||
import { roles } from "./staff";
|
||||
import { showStudioJoin } from "./studios";
|
||||
import { externalid, image, language, schema } from "./utils";
|
||||
import { externalid, image, language, schema, timestamp } from "./utils";
|
||||
|
||||
export const showKind = schema.enum("show_kind", [
|
||||
"serie",
|
||||
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { Character } from "~/models/staff";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { externalid, image, schema } from "./utils";
|
||||
import { externalid, image, schema, timestamp } from "./utils";
|
||||
|
||||
export const roleKind = schema.enum("role_kind", [
|
||||
"actor",
|
||||
|
||||
@@ -7,9 +7,8 @@ import {
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { shows } from "./shows";
|
||||
import { externalid, image, language, schema } from "./utils";
|
||||
import { externalid, image, language, schema, timestamp } from "./utils";
|
||||
|
||||
export const studios = schema.table("studios", {
|
||||
pk: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { jsonb, pgSchema, varchar } from "drizzle-orm/pg-core";
|
||||
import { customType, jsonb, pgSchema, varchar } from "drizzle-orm/pg-core";
|
||||
import type { Image } from "~/models/utils";
|
||||
|
||||
export const schema = pgSchema("kyoo");
|
||||
@@ -20,3 +20,19 @@ export const externalid = () =>
|
||||
>()
|
||||
.notNull()
|
||||
.default({});
|
||||
|
||||
export const timestamp = customType<{
|
||||
data: string;
|
||||
driverData: string;
|
||||
config: { withTimezone: boolean; precision?: number; mode: "iso" };
|
||||
}>({
|
||||
dataType(config) {
|
||||
const precision = config?.precision ? ` (${config.precision})` : "";
|
||||
return `timestamp${precision}${config?.withTimezone ? " with time zone" : ""}`;
|
||||
},
|
||||
fromDriver(value: string): string {
|
||||
// postgres format: 2025-06-22 16:13:37.489301+00
|
||||
// what we want: 2025-06-22T16:13:37Z
|
||||
return `${value.substring(0, 10)}T${value.substring(11, 19)}Z`;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,9 +10,8 @@ import {
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { Guess } from "~/models/video";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { schema } from "./utils";
|
||||
import { schema, timestamp } from "./utils";
|
||||
|
||||
export const videos = schema.table(
|
||||
"videos",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, integer, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { timestamp } from "../utils";
|
||||
import { entries } from "./entries";
|
||||
import { profiles } from "./profiles";
|
||||
import { shows } from "./shows";
|
||||
import { schema } from "./utils";
|
||||
import { schema, timestamp } from "./utils";
|
||||
|
||||
export const watchlistStatus = schema.enum("watchlist_status", [
|
||||
"watching",
|
||||
|
||||
@@ -8,15 +8,16 @@ import {
|
||||
type Subquery,
|
||||
sql,
|
||||
Table,
|
||||
type TableConfig,
|
||||
View,
|
||||
ViewBaseConfig,
|
||||
} from "drizzle-orm";
|
||||
import type { CasingCache } from "drizzle-orm/casing";
|
||||
import type { AnyMySqlSelect } from "drizzle-orm/mysql-core";
|
||||
import {
|
||||
type AnyPgSelect,
|
||||
customType,
|
||||
type SelectedFieldsFlat,
|
||||
import type {
|
||||
AnyPgSelect,
|
||||
PgTableWithColumns,
|
||||
SelectedFieldsFlat,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { AnySQLiteSelect } from "drizzle-orm/sqlite-core";
|
||||
import type { WithSubquery } from "drizzle-orm/subquery";
|
||||
@@ -73,8 +74,18 @@ export function conflictUpdateAllExcept<
|
||||
}
|
||||
|
||||
// drizzle is bugged and doesn't allow js arrays to be used in raw sql.
|
||||
export function sqlarr(array: unknown[]) {
|
||||
return `{${array.map((item) => `"${item}"`).join(",")}}`;
|
||||
export function sqlarr(array: unknown[]): string {
|
||||
return `{${array
|
||||
.map((item) =>
|
||||
item === "null" || item === null || item === undefined
|
||||
? "null"
|
||||
: Array.isArray(item)
|
||||
? sqlarr(item)
|
||||
: typeof item === "object"
|
||||
? `"${JSON.stringify(item).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
|
||||
: `"${item?.toString().replaceAll('"', '\\"')}"`,
|
||||
)
|
||||
.join(", ")}}`;
|
||||
}
|
||||
|
||||
// See https://github.com/drizzle-team/drizzle-orm/issues/4044
|
||||
@@ -107,7 +118,103 @@ export function values<K extends string>(
|
||||
};
|
||||
}
|
||||
|
||||
export const coalesce = <T>(val: SQL<T> | Column, def: SQL<T> | Column) => {
|
||||
/* goal:
|
||||
* unnestValues([{a: 1, b: 2}, {a: 3, b: 4}], tbl)
|
||||
*
|
||||
* ```sql
|
||||
* select a, b, now() as updated_at from unnest($1::integer[], $2::integer[]);
|
||||
* ```
|
||||
* params:
|
||||
* $1: [1, 2]
|
||||
* $2: [3, 4]
|
||||
*
|
||||
* select
|
||||
*/
|
||||
export const unnestValues = <
|
||||
T extends Record<string, unknown>,
|
||||
C extends TableConfig = never,
|
||||
>(
|
||||
values: T[],
|
||||
typeInfo: PgTableWithColumns<C>,
|
||||
) => {
|
||||
if (values[0] === undefined)
|
||||
throw new Error("Invalid values, expecting at least one items");
|
||||
|
||||
const columns = getTableColumns(typeInfo);
|
||||
const keys = Object.keys(values[0]).filter((x) => x in columns);
|
||||
// @ts-expect-error: drizzle internal
|
||||
const casing = db.dialect.casing as CasingCache;
|
||||
const dbNames = Object.fromEntries(
|
||||
Object.entries(columns).map(([k, v]) => [k, casing.getColumnCasing(v)]),
|
||||
);
|
||||
const vals = values.reduce(
|
||||
(acc, cur, i) => {
|
||||
for (const k of keys) {
|
||||
if (k in cur) acc[k].push(cur[k]);
|
||||
else acc[k].push(null);
|
||||
}
|
||||
for (const k of Object.keys(cur)) {
|
||||
if (k in acc) continue;
|
||||
if (!(k in columns)) continue;
|
||||
keys.push(k);
|
||||
acc[k] = new Array(i).fill(null);
|
||||
acc[k].push(cur[k]);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
Object.fromEntries(keys.map((x) => [x, [] as unknown[]])),
|
||||
);
|
||||
const computed = Object.entries(columns)
|
||||
.filter(([k, v]) => (v.defaultFn || v.onUpdateFn) && !keys.includes(k))
|
||||
.map(([k]) => k);
|
||||
return db
|
||||
.select(
|
||||
Object.fromEntries([
|
||||
...keys.map((x) => [x, sql.raw(`"${dbNames[x]}"`)]),
|
||||
...computed.map((x) => [
|
||||
x,
|
||||
(columns[x].defaultFn?.() ?? columns[x].onUpdateFn!()).as(dbNames[x]),
|
||||
]),
|
||||
]) as {
|
||||
[k in keyof typeof typeInfo.$inferInsert]-?: SQL.Aliased<
|
||||
(typeof typeInfo.$inferInsert)[k]
|
||||
>;
|
||||
},
|
||||
)
|
||||
.from(
|
||||
sql`unnest(${sql.join(
|
||||
keys.map(
|
||||
(k) =>
|
||||
sql`${sqlarr(vals[k])}${sql.raw(`::${columns[k].getSQLType()}[]`)}`,
|
||||
),
|
||||
sql.raw(", "),
|
||||
)}) as v(${sql.raw(keys.map((x) => `"${dbNames[x]}"`).join(", "))})`,
|
||||
);
|
||||
};
|
||||
|
||||
export const unnest = <T extends Record<string, unknown>>(
|
||||
values: T[],
|
||||
name: string,
|
||||
typeInfo: Record<keyof T, string>,
|
||||
) => {
|
||||
const keys = Object.keys(typeInfo);
|
||||
const vals = values.reduce(
|
||||
(acc, cur) => {
|
||||
for (const k of keys) {
|
||||
if (k in cur) acc[k].push(cur[k]);
|
||||
else acc[k].push(null);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
Object.fromEntries(keys.map((x) => [x, [] as unknown[]])),
|
||||
);
|
||||
return sql`unnest(${sql.join(
|
||||
keys.map((k) => sql`${sqlarr(vals[k])}${sql.raw(`::${typeInfo[k]}[]`)}`),
|
||||
sql.raw(", "),
|
||||
)}) as ${sql.raw(name)}(${sql.raw(keys.map((x) => `"${x}"`).join(", "))})`;
|
||||
};
|
||||
|
||||
export const coalesce = <T>(val: SQL<T> | SQLWrapper, def: SQL<T> | Column) => {
|
||||
return sql<T>`coalesce(${val}, ${def})`;
|
||||
};
|
||||
|
||||
@@ -148,23 +255,12 @@ export const jsonbBuildObject = <T>(select: JsonFields) => {
|
||||
};
|
||||
|
||||
export const isUniqueConstraint = (e: unknown): boolean => {
|
||||
if (typeof e !== "object" || !e || !("cause" in e)) return false;
|
||||
const cause = e.cause;
|
||||
return (
|
||||
typeof e === "object" && e != null && "code" in e && e.code === "23505"
|
||||
typeof cause === "object" &&
|
||||
cause != null &&
|
||||
"code" in cause &&
|
||||
cause.code === "23505"
|
||||
);
|
||||
};
|
||||
|
||||
export const timestamp = customType<{
|
||||
data: string;
|
||||
driverData: string;
|
||||
config: { withTimezone: boolean; precision?: number; mode: "iso" };
|
||||
}>({
|
||||
dataType(config) {
|
||||
const precision = config?.precision ? ` (${config.precision})` : "";
|
||||
return `timestamp${precision}${config?.withTimezone ? " with time zone" : ""}`;
|
||||
},
|
||||
fromDriver(value: string): string {
|
||||
// postgres format: 2025-06-22 16:13:37.489301+00
|
||||
// what we want: 2025-06-22T16:13:37Z
|
||||
return `${value.substring(0, 10)}T${value.substring(11, 19)}Z`;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,15 +3,13 @@ import { comment } from "~/utils";
|
||||
|
||||
export const Progress = t.Object({
|
||||
percent: t.Integer({ minimum: 0, maximum: 100 }),
|
||||
time: t.Nullable(
|
||||
t.Integer({
|
||||
minimum: 0,
|
||||
description: comment`
|
||||
time: t.Integer({
|
||||
minimum: 0,
|
||||
description: comment`
|
||||
When this episode was stopped (in seconds since the start).
|
||||
This value is null if the entry was never watched or is finished.
|
||||
`,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
playedDate: t.Nullable(t.String({ format: "date-time" })),
|
||||
videoId: t.Nullable(
|
||||
t.String({
|
||||
|
||||
@@ -18,11 +18,31 @@ export const Page = <T extends TSchema>(schema: T, options?: ObjectOptions) =>
|
||||
|
||||
export const createPage = <T>(
|
||||
items: T[],
|
||||
{ url, sort, limit }: { url: string; sort: Sort; limit: number },
|
||||
{
|
||||
url,
|
||||
sort,
|
||||
limit,
|
||||
headers,
|
||||
}: {
|
||||
url: string;
|
||||
sort: Sort;
|
||||
limit: number;
|
||||
headers?: Record<string, string | undefined>;
|
||||
},
|
||||
) => {
|
||||
let next: string | null = null;
|
||||
const uri = new URL(url);
|
||||
const forwardedProto = headers?.["x-forwarded-proto"];
|
||||
if (forwardedProto) {
|
||||
uri.protocol = forwardedProto;
|
||||
}
|
||||
const forwardedHost = headers?.["x-forwarded-host"];
|
||||
if (forwardedHost) {
|
||||
uri.host = forwardedHost;
|
||||
}
|
||||
|
||||
const current = uri.toString();
|
||||
|
||||
let next: string | null = null;
|
||||
if (sort.random) {
|
||||
uri.searchParams.set("sort", `random:${sort.random.seed}`);
|
||||
url = uri.toString();
|
||||
@@ -35,5 +55,5 @@ export const createPage = <T>(
|
||||
uri.searchParams.set("after", generateAfter(items[items.length - 1], sort));
|
||||
next = uri.toString();
|
||||
}
|
||||
return { items, this: url, next };
|
||||
return { items, this: current, next };
|
||||
};
|
||||
|
||||
@@ -55,10 +55,11 @@ export const Sort = (
|
||||
),
|
||||
)
|
||||
.Decode((sort: string[]): Sort => {
|
||||
if (!Array.isArray(sort)) sort = [sort];
|
||||
const random = sort.find((x) => x.startsWith("random"));
|
||||
if (random) {
|
||||
const seed = random.includes(":")
|
||||
? Number.parseInt(random.substring("random:".length))
|
||||
? Number.parseInt(random.substring("random:".length), 10)
|
||||
: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
return { tablePk, random: { seed }, sort: [] };
|
||||
}
|
||||
|
||||
43
api/src/otel.ts
Normal file
43
api/src/otel.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { record as elysiaRecord, opentelemetry } from "@elysiajs/opentelemetry";
|
||||
import { OTLPMetricExporter as GrpcMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
||||
import { OTLPMetricExporter as HttpMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
||||
import { OTLPTraceExporter as GrpcTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
||||
import { OTLPTraceExporter as HttpTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
||||
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
||||
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
||||
import Elysia from "elysia";
|
||||
|
||||
const protocol =
|
||||
process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ??
|
||||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL ??
|
||||
"http/protobuf";
|
||||
|
||||
export const otel = new Elysia()
|
||||
.use(
|
||||
opentelemetry({
|
||||
serviceName: "kyoo.api",
|
||||
spanProcessors: [
|
||||
new BatchSpanProcessor(
|
||||
protocol === "grpc"
|
||||
? new GrpcTraceExporter()
|
||||
: new HttpTraceExporter(),
|
||||
),
|
||||
],
|
||||
metricReader: new PeriodicExportingMetricReader({
|
||||
exporter:
|
||||
protocol === "grpc"
|
||||
? new GrpcMetricExporter()
|
||||
: new HttpMetricExporter(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.as("global");
|
||||
|
||||
export function record<T extends (...args: any) => any>(
|
||||
spanName: string,
|
||||
fn: T,
|
||||
): T {
|
||||
const wrapped = (...args: Parameters<T>) =>
|
||||
elysiaRecord(spanName, () => fn(...args));
|
||||
return wrapped as T;
|
||||
}
|
||||
48
api/tests/helpers/collections-helper.ts
Normal file
48
api/tests/helpers/collections-helper.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { buildUrl } from "tests/utils";
|
||||
import { handlers } from "~/base";
|
||||
import { getJwtHeaders } from "./jwt";
|
||||
|
||||
export const getCollection = async (
|
||||
id: string,
|
||||
{
|
||||
langs,
|
||||
...query
|
||||
}: { langs?: string; preferOriginal?: boolean; with?: string[] },
|
||||
) => {
|
||||
const resp = await handlers.handle(
|
||||
new Request(buildUrl(`collections/${id}`, query), {
|
||||
method: "GET",
|
||||
headers: langs
|
||||
? {
|
||||
"Accept-Language": langs,
|
||||
...(await getJwtHeaders()),
|
||||
}
|
||||
: await getJwtHeaders(),
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
return [resp, body] as const;
|
||||
};
|
||||
|
||||
export const getCollections = async ({
|
||||
langs,
|
||||
...query
|
||||
}: {
|
||||
langs?: string;
|
||||
preferOriginal?: boolean;
|
||||
with?: string[];
|
||||
}) => {
|
||||
const resp = await handlers.handle(
|
||||
new Request(buildUrl("collections", query), {
|
||||
method: "GET",
|
||||
headers: langs
|
||||
? {
|
||||
"Accept-Language": langs,
|
||||
...(await getJwtHeaders()),
|
||||
}
|
||||
: await getJwtHeaders(),
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
return [resp, body] as const;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "~/base";
|
||||
export * from "./collections-helper";
|
||||
export * from "./movies-helper";
|
||||
export * from "./series-helper";
|
||||
export * from "./shows-helper";
|
||||
|
||||
@@ -20,6 +20,7 @@ const [resp, body] = await createVideo([
|
||||
title: "mia",
|
||||
episodes: [{ season: 1, episode: 13 }],
|
||||
from: "test",
|
||||
history: [],
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13.mkv",
|
||||
@@ -33,6 +34,7 @@ const [resp, body] = await createVideo([
|
||||
episodes: [{ season: 2, episode: 1 }],
|
||||
years: [2017],
|
||||
from: "test",
|
||||
history: [],
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia 2017 s2e1.mkv",
|
||||
@@ -41,7 +43,7 @@ const [resp, body] = await createVideo([
|
||||
for: [{ slug: `${madeInAbyss.slug}-s2e1` }],
|
||||
},
|
||||
{
|
||||
guess: { title: "bubble", from: "test" },
|
||||
guess: { title: "bubble", from: "test", history: [] },
|
||||
part: null,
|
||||
path: "/video/bubble.mkv",
|
||||
rendering: "sha5",
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { createMovie, createSerie } from "tests/helpers";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { defaultBlurhash, processImages } from "~/controllers/seed/images";
|
||||
import { db } from "~/db";
|
||||
import { mqueue, shows, staff, studios, videos } from "~/db/schema";
|
||||
import { madeInAbyss } from "~/models/examples";
|
||||
import { createSerie } from "../helpers";
|
||||
import { dune, madeInAbyss } from "~/models/examples";
|
||||
|
||||
describe("images", () => {
|
||||
it("Create a serie download images", async () => {
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
await db.delete(studios);
|
||||
await db.delete(staff);
|
||||
await db.delete(videos);
|
||||
await db.delete(mqueue);
|
||||
});
|
||||
|
||||
it("Create a serie download images", async () => {
|
||||
await db.delete(mqueue);
|
||||
await createSerie(madeInAbyss);
|
||||
const release = await processImages();
|
||||
// remove notifications to prevent other images to be downloaded (do not curl 20000 images for nothing)
|
||||
@@ -26,4 +30,34 @@ describe("images", () => {
|
||||
expect(ret!.original.poster!.blurhash).toBeString();
|
||||
expect(ret!.original.poster!.blurhash).not.toBe(defaultBlurhash);
|
||||
});
|
||||
|
||||
it("Download 404 image", async () => {
|
||||
await db.delete(mqueue);
|
||||
const url404 = "https://mockhttp.org/status/404";
|
||||
const [ret, body] = await createMovie({
|
||||
...dune,
|
||||
translations: {
|
||||
en: {
|
||||
...dune.translations.en,
|
||||
poster: url404,
|
||||
thumbnail: null,
|
||||
banner: null,
|
||||
logo: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expectStatus(ret, body).toBe(201);
|
||||
|
||||
const release = await processImages();
|
||||
// remove notifications to prevent other images to be downloaded (do not curl 20000 images for nothing)
|
||||
release();
|
||||
|
||||
const failed = await db.query.mqueue.findFirst({
|
||||
where: and(
|
||||
eq(mqueue.kind, "image"),
|
||||
eq(sql`${mqueue.message}->>'url'`, url404),
|
||||
),
|
||||
});
|
||||
expect(failed!.attempt).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
106
api/tests/misc/x-forwarded-headers.test.ts
Normal file
106
api/tests/misc/x-forwarded-headers.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { getJwtHeaders } from "tests/helpers/jwt";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { shows } from "~/db/schema";
|
||||
import { bubble } from "~/models/examples";
|
||||
import { dune1984 } from "~/models/examples/dune-1984";
|
||||
import { dune } from "~/models/examples/dune-2021";
|
||||
import { createMovie, getMovies, handlers } from "../helpers";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
for (const movie of [bubble, dune1984, dune]) {
|
||||
const [ret, _] = await createMovie(movie);
|
||||
expect(ret.status).toBe(201);
|
||||
}
|
||||
});
|
||||
|
||||
describe("X-Forwarded-Proto header support", () => {
|
||||
it("Pagination URLs use HTTPS when X-Forwarded-Proto is https", async () => {
|
||||
const resp = await handlers.handle(
|
||||
new Request("http://localhost/api/movies?limit=2", {
|
||||
headers: {
|
||||
...(await getJwtHeaders()),
|
||||
"x-forwarded-proto": "https",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
items: expect.any(Array),
|
||||
this: "https://localhost/api/movies?limit=2",
|
||||
next: expect.stringContaining("https://localhost/api/movies?limit=2"),
|
||||
});
|
||||
});
|
||||
|
||||
it("Pagination URLs use HTTP when no X-Forwarded-Proto header", async () => {
|
||||
const [resp, body] = await getMovies({
|
||||
limit: 2,
|
||||
langs: "en",
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
items: expect.any(Array),
|
||||
this: "http://localhost/api/movies?limit=2",
|
||||
next: expect.stringContaining("http://localhost/api/movies?limit=2"),
|
||||
});
|
||||
});
|
||||
|
||||
it("X-Forwarded-Host header changes the host in pagination URLs", async () => {
|
||||
const resp = await handlers.handle(
|
||||
new Request("http://localhost/api/movies?limit=2", {
|
||||
headers: {
|
||||
...(await getJwtHeaders()),
|
||||
"x-forwarded-proto": "https",
|
||||
"x-forwarded-host": "kyoo.example.com",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
items: expect.any(Array),
|
||||
this: "https://kyoo.example.com/api/movies?limit=2",
|
||||
next: expect.stringContaining(
|
||||
"https://kyoo.example.com/api/movies?limit=2",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("Second page of pagination respects X-Forwarded headers", async () => {
|
||||
let resp = await handlers.handle(
|
||||
new Request("http://localhost/api/movies?limit=2", {
|
||||
headers: {
|
||||
...(await getJwtHeaders()),
|
||||
"x-forwarded-proto": "https",
|
||||
"x-forwarded-host": "kyoo.example.com",
|
||||
},
|
||||
}),
|
||||
);
|
||||
let body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body.next).toBeTruthy();
|
||||
expect(body.next).toContain("https://kyoo.example.com");
|
||||
|
||||
// Follow the next link with the same headers
|
||||
resp = await handlers.handle(
|
||||
new Request(body.next, {
|
||||
headers: {
|
||||
...(await getJwtHeaders()),
|
||||
"x-forwarded-proto": "https",
|
||||
"x-forwarded-host": "kyoo.example.com",
|
||||
},
|
||||
}),
|
||||
);
|
||||
body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body.this).toContain("https://kyoo.example.com");
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
import { beforeAll } from "bun:test";
|
||||
import { migrate } from "~/db";
|
||||
|
||||
process.env.PGDATABASE = "kyoo_test";
|
||||
process.env.JWT_SECRET = "this is a secret";
|
||||
process.env.JWT_ISSUER = "https://kyoo.zoriya.dev";
|
||||
process.env.IMAGES_PATH = "./images";
|
||||
|
||||
beforeAll(async () => {
|
||||
// lazy load this so env set before actually applies
|
||||
const { migrate } = await import("~/db");
|
||||
await migrate();
|
||||
});
|
||||
|
||||
@@ -591,4 +591,127 @@ describe("Video seeding", () => {
|
||||
expect(vid!.evj[1].slug).toBe("made-in-abyss-s2e1");
|
||||
expect(vid!.evj[1].entry.slug).toBe("made-in-abyss-s2e1");
|
||||
});
|
||||
|
||||
it("work with duplicated episodes", async () => {
|
||||
await db.delete(videos);
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
title: "mia",
|
||||
episodes: [
|
||||
{ season: 1, episode: 13 },
|
||||
{ season: 1, episode: 13 },
|
||||
],
|
||||
from: "test",
|
||||
history: [],
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13.mkv",
|
||||
rendering: "duptest",
|
||||
version: 1,
|
||||
for: [
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
],
|
||||
});
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(1);
|
||||
expect(body[0].id).toBeString();
|
||||
|
||||
const vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia s1e13.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13");
|
||||
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-s1e13");
|
||||
});
|
||||
|
||||
it("work with duplicated two episodes", async () => {
|
||||
await db.delete(videos);
|
||||
const [resp, body] = await createVideo([
|
||||
{
|
||||
guess: {
|
||||
title: "mia",
|
||||
episodes: [
|
||||
{ season: 1, episode: 13 },
|
||||
{ season: 1, episode: 13 },
|
||||
],
|
||||
from: "test",
|
||||
history: [],
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13.mkv",
|
||||
rendering: "duptest-two",
|
||||
version: 1,
|
||||
for: [
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
],
|
||||
},
|
||||
{
|
||||
guess: {
|
||||
title: "mia",
|
||||
episodes: [
|
||||
{ season: 1, episode: 13 },
|
||||
{ season: 1, episode: 13 },
|
||||
],
|
||||
from: "test",
|
||||
history: [],
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13 bis.mkv",
|
||||
rendering: "duptest-two-bis",
|
||||
version: 1,
|
||||
for: [
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(2);
|
||||
expect(body[0].id).toBeString();
|
||||
expect(body[1].id).toBeString();
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(body[0].entries[0].slug).toBe("made-in-abyss-s1e13");
|
||||
expect(body[1].entries).toBeArrayOfSize(1);
|
||||
expect(body[1].entries[0].slug).toBe("made-in-abyss-s1e13-duptest-two-bis");
|
||||
|
||||
let vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[0].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia s1e13.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13");
|
||||
|
||||
vid = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, body[1].id),
|
||||
with: {
|
||||
evj: { with: { entry: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/mia s1e13 bis.mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13-duptest-two-bis");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"target": "ES2021",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"types": ["bun-types"],
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
**
|
||||
!/go.mod
|
||||
!/go.sum
|
||||
!/**.go
|
||||
!/**/*.go
|
||||
# generated via sqlc
|
||||
!/sql
|
||||
!/dbc
|
||||
|
||||
@@ -19,12 +19,15 @@ PROTECTED_CLAIMS="permissions"
|
||||
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
||||
PUBLIC_URL=http://localhost:8901
|
||||
|
||||
# You can create apikeys at runtime via POST /apikey but you can also have some defined in the env.
|
||||
# You can create apikeys at runtime via POST /key but you can also have some defined in the env.
|
||||
# Replace $YOURNAME with the name of the key you want (only alpha are valid)
|
||||
# The value will be the apikey (max 128 bytes)
|
||||
# KEIBI_APIKEY_$YOURNAME=oaeushtaoesunthoaensuth
|
||||
# KEIBI_APIKEY_$YOURNAME_CLAIMS='{"permissions": ["users.read"]}'
|
||||
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
|
||||
|
||||
# Database things
|
||||
# It is recommended to use the below PG environment variables when possible.
|
||||
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
|
||||
@@ -43,8 +46,3 @@ PGPORT=5432
|
||||
# PGSSLROOTCERT=/my/serving.crt
|
||||
# PGSSLCERT=/my/client.crt
|
||||
# PGSSLKEY=/my/client.key
|
||||
|
||||
# Default is keibi, you can specify "disabled" to use the default search_path of the user.
|
||||
# If this is not "disabled", the schema will be created (if it does not exists) and
|
||||
# the search_path of the user will be ignored (only the schema specified will be used).
|
||||
POSTGRES_SCHEMA=keibi
|
||||
|
||||
@@ -60,8 +60,8 @@ GET `/users/$id/sessions` can be used by admins to list others session
|
||||
### Api keys
|
||||
|
||||
```
|
||||
Get `/apikeys`
|
||||
Post `/apikeys` {...claims} Create a new api keys with given claims
|
||||
Get `/keys`
|
||||
Post `/keys` {...claims} Create a new api keys with given claims
|
||||
```
|
||||
|
||||
An api key can be used like an opaque token, calling /jwt with it will return a valid jwt with the claims you specified during the post request to create it.
|
||||
|
||||
@@ -4,10 +4,9 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"strings"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -45,7 +44,7 @@ func MapDbKey(key *dbc.Apikey) ApiKeyWToken {
|
||||
CreatedAt: key.CreatedAt,
|
||||
LastUsed: key.LastUsed,
|
||||
},
|
||||
Token: fmt.Sprintf("%s-%s", key.Name, key.Token),
|
||||
Token: key.Token,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +74,10 @@ func (h *Handler) CreateApiKey(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, conflict := h.config.EnvApiKeys[req.Name]; conflict {
|
||||
conflict := slices.ContainsFunc(h.config.EnvApiKeys, func(k ApiKeyWToken) bool {
|
||||
return k.Name == req.Name
|
||||
})
|
||||
if conflict {
|
||||
return echo.NewHTTPError(409, "An env apikey is already defined with the same name")
|
||||
}
|
||||
|
||||
@@ -174,17 +176,15 @@ func (h *Handler) ListApiKey(c echo.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handler) createApiJwt(apikey string) (string, error) {
|
||||
info := strings.SplitN(apikey, "-", 2)
|
||||
if len(info) != 2 {
|
||||
return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key format")
|
||||
var key *ApiKeyWToken
|
||||
for _, k := range h.config.EnvApiKeys {
|
||||
if k.Token == apikey {
|
||||
key = &k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
key, fromEnv := h.config.EnvApiKeys[info[0]]
|
||||
if !fromEnv {
|
||||
dbKey, err := h.db.GetApiKey(context.Background(), dbc.GetApiKeyParams{
|
||||
Name: info[0],
|
||||
Token: info[1],
|
||||
})
|
||||
if key == nil {
|
||||
dbKey, err := h.db.GetApiKey(context.Background(), apikey)
|
||||
if err == pgx.ErrNoRows {
|
||||
return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key")
|
||||
} else if err != nil {
|
||||
@@ -195,7 +195,8 @@ func (h *Handler) createApiJwt(apikey string) (string, error) {
|
||||
h.db.TouchApiKey(context.Background(), dbKey.Pk)
|
||||
}()
|
||||
|
||||
key = MapDbKey(&dbKey)
|
||||
found := MapDbKey(&dbKey)
|
||||
key = &found
|
||||
}
|
||||
|
||||
claims := maps.Clone(key.Claims)
|
||||
@@ -210,6 +211,7 @@ func (h *Handler) createApiJwt(apikey string) (string, error) {
|
||||
Time: time.Now().UTC().Add(time.Hour),
|
||||
}
|
||||
jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
jwt.Header["kid"] = h.config.JwtKid
|
||||
t, err := jwt.SignedString(h.config.JwtPrivateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -31,7 +32,7 @@ type Configuration struct {
|
||||
GuestClaims jwt.MapClaims
|
||||
ProtectedClaims []string
|
||||
ExpirationDelay time.Duration
|
||||
EnvApiKeys map[string]ApiKeyWToken
|
||||
EnvApiKeys []ApiKeyWToken
|
||||
}
|
||||
|
||||
var DefaultConfig = Configuration{
|
||||
@@ -39,7 +40,7 @@ var DefaultConfig = Configuration{
|
||||
FirstUserClaims: make(jwt.MapClaims),
|
||||
ProtectedClaims: []string{"permissions"},
|
||||
ExpirationDelay: 30 * 24 * time.Hour,
|
||||
EnvApiKeys: make(map[string]ApiKeyWToken),
|
||||
EnvApiKeys: make([]ApiKeyWToken, 0),
|
||||
}
|
||||
|
||||
func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
||||
@@ -137,14 +138,14 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
||||
}
|
||||
|
||||
name = strings.ToLower(name)
|
||||
ret.EnvApiKeys[name] = ApiKeyWToken{
|
||||
ret.EnvApiKeys = append(ret.EnvApiKeys, ApiKeyWToken{
|
||||
ApiKey: ApiKey{
|
||||
Id: uuid.New(),
|
||||
Name: name,
|
||||
Claims: claims,
|
||||
},
|
||||
Token: v[1],
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
apikeys, err := db.ListApiKeys(context.Background())
|
||||
@@ -152,7 +153,10 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, key := range apikeys {
|
||||
if _, defined := ret.EnvApiKeys[key.Name]; defined {
|
||||
dup := slices.ContainsFunc(ret.EnvApiKeys, func(k ApiKeyWToken) bool {
|
||||
return k.Name == key.Name
|
||||
})
|
||||
if dup {
|
||||
return nil, fmt.Errorf(
|
||||
"an api key with the name %s is already defined in database. Can't specify a new one via env var",
|
||||
key.Name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: apikeys.sql
|
||||
|
||||
package dbc
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const createApiKey = `-- name: CreateApiKey :one
|
||||
insert into apikeys(name, token, claims, created_by)
|
||||
insert into keibi.apikeys(name, token, claims, created_by)
|
||||
values ($1, $2, $3, $4)
|
||||
returning
|
||||
pk, id, name, token, claims, created_by, created_at, last_used
|
||||
@@ -48,7 +48,7 @@ func (q *Queries) CreateApiKey(ctx context.Context, arg CreateApiKeyParams) (Api
|
||||
}
|
||||
|
||||
const deleteApiKey = `-- name: DeleteApiKey :one
|
||||
delete from apikeys
|
||||
delete from keibi.apikeys
|
||||
where id = $1
|
||||
returning
|
||||
pk, id, name, token, claims, created_by, created_at, last_used
|
||||
@@ -74,19 +74,13 @@ const getApiKey = `-- name: GetApiKey :one
|
||||
select
|
||||
pk, id, name, token, claims, created_by, created_at, last_used
|
||||
from
|
||||
apikeys
|
||||
keibi.apikeys
|
||||
where
|
||||
name = $1
|
||||
and token = $2
|
||||
token = $1
|
||||
`
|
||||
|
||||
type GetApiKeyParams struct {
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetApiKey(ctx context.Context, arg GetApiKeyParams) (Apikey, error) {
|
||||
row := q.db.QueryRow(ctx, getApiKey, arg.Name, arg.Token)
|
||||
func (q *Queries) GetApiKey(ctx context.Context, token string) (Apikey, error) {
|
||||
row := q.db.QueryRow(ctx, getApiKey, token)
|
||||
var i Apikey
|
||||
err := row.Scan(
|
||||
&i.Pk,
|
||||
@@ -105,7 +99,7 @@ const listApiKeys = `-- name: ListApiKeys :many
|
||||
select
|
||||
pk, id, name, token, claims, created_by, created_at, last_used
|
||||
from
|
||||
apikeys
|
||||
keibi.apikeys
|
||||
order by
|
||||
last_used
|
||||
`
|
||||
@@ -141,7 +135,7 @@ func (q *Queries) ListApiKeys(ctx context.Context) ([]Apikey, error) {
|
||||
|
||||
const touchApiKey = `-- name: TouchApiKey :exec
|
||||
update
|
||||
apikeys
|
||||
keibi.apikeys
|
||||
set
|
||||
last_used = now()::timestamptz
|
||||
where
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
|
||||
package dbc
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
|
||||
package dbc
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: sessions.sql
|
||||
|
||||
package dbc
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const clearOtherSessions = `-- name: ClearOtherSessions :exec
|
||||
delete from sessions as s using users as u
|
||||
delete from keibi.sessions as s using keibi.users as u
|
||||
where s.user_pk = u.pk
|
||||
and s.id != $1
|
||||
and u.id = $2
|
||||
@@ -30,7 +30,7 @@ func (q *Queries) ClearOtherSessions(ctx context.Context, arg ClearOtherSessions
|
||||
}
|
||||
|
||||
const createSession = `-- name: CreateSession :one
|
||||
insert into sessions(token, user_pk, device)
|
||||
insert into keibi.sessions(token, user_pk, device)
|
||||
values ($1, $2, $3)
|
||||
returning
|
||||
pk, id, token, user_pk, created_date, last_used, device
|
||||
@@ -58,7 +58,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
|
||||
}
|
||||
|
||||
const deleteSession = `-- name: DeleteSession :one
|
||||
delete from sessions as s using users as u
|
||||
delete from keibi.sessions as s using keibi.users as u
|
||||
where s.user_pk = u.pk
|
||||
and s.id = $1
|
||||
and u.id = $2
|
||||
@@ -93,8 +93,8 @@ select
|
||||
s.last_used,
|
||||
u.pk, u.id, u.username, u.email, u.password, u.claims, u.created_date, u.last_seen
|
||||
from
|
||||
users as u
|
||||
inner join sessions as s on u.pk = s.user_pk
|
||||
keibi.users as u
|
||||
inner join keibi.sessions as s on u.pk = s.user_pk
|
||||
where
|
||||
s.token = $1
|
||||
limit 1
|
||||
@@ -130,8 +130,8 @@ const getUserSessions = `-- name: GetUserSessions :many
|
||||
select
|
||||
s.pk, s.id, s.token, s.user_pk, s.created_date, s.last_used, s.device
|
||||
from
|
||||
sessions as s
|
||||
inner join users as u on u.pk = s.user_pk
|
||||
keibi.sessions as s
|
||||
inner join keibi.users as u on u.pk = s.user_pk
|
||||
where
|
||||
u.pk = $1
|
||||
order by
|
||||
@@ -168,7 +168,7 @@ func (q *Queries) GetUserSessions(ctx context.Context, pk int32) ([]Session, err
|
||||
|
||||
const touchSession = `-- name: TouchSession :exec
|
||||
update
|
||||
sessions
|
||||
keibi.sessions
|
||||
set
|
||||
last_used = now()::timestamptz
|
||||
where
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: users.sql
|
||||
|
||||
package dbc
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
insert into users(username, email, password, claims)
|
||||
insert into keibi.users(username, email, password, claims)
|
||||
values ($1, $2, $3, case when not exists (
|
||||
select
|
||||
pk, id, username, email, password, claims, created_date, last_seen
|
||||
from
|
||||
users) then
|
||||
keibi.users) then
|
||||
$4::jsonb
|
||||
else
|
||||
$5::jsonb
|
||||
@@ -58,7 +58,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
}
|
||||
|
||||
const deleteUser = `-- name: DeleteUser :one
|
||||
delete from users
|
||||
delete from keibi.users
|
||||
where id = $1
|
||||
returning
|
||||
pk, id, username, email, password, claims, created_date, last_seen
|
||||
@@ -84,7 +84,7 @@ const getAllUsers = `-- name: GetAllUsers :many
|
||||
select
|
||||
pk, id, username, email, password, claims, created_date, last_seen
|
||||
from
|
||||
users
|
||||
keibi.users
|
||||
order by
|
||||
id
|
||||
limit $1
|
||||
@@ -123,7 +123,7 @@ const getAllUsersAfter = `-- name: GetAllUsersAfter :many
|
||||
select
|
||||
pk, id, username, email, password, claims, created_date, last_seen
|
||||
from
|
||||
users
|
||||
keibi.users
|
||||
where
|
||||
id >= $2
|
||||
order by
|
||||
@@ -173,8 +173,8 @@ select
|
||||
h.username,
|
||||
h.profile_url
|
||||
from
|
||||
users as u
|
||||
left join oidc_handle as h on u.pk = h.user_pk
|
||||
keibi.users as u
|
||||
left join keibi.oidc_handle as h on u.pk = h.user_pk
|
||||
where ($1::boolean
|
||||
and u.id = $2)
|
||||
or (not $1
|
||||
@@ -232,7 +232,7 @@ const getUserByLogin = `-- name: GetUserByLogin :one
|
||||
select
|
||||
pk, id, username, email, password, claims, created_date, last_seen
|
||||
from
|
||||
users
|
||||
keibi.users
|
||||
where
|
||||
email = $1
|
||||
or username = $1
|
||||
@@ -257,9 +257,9 @@ func (q *Queries) GetUserByLogin(ctx context.Context, login string) (User, error
|
||||
|
||||
const touchUser = `-- name: TouchUser :exec
|
||||
update
|
||||
users
|
||||
keibi.users
|
||||
set
|
||||
last_used = now()::timestamptz
|
||||
last_seen = now()::timestamptz
|
||||
where
|
||||
pk = $1
|
||||
`
|
||||
@@ -271,12 +271,12 @@ func (q *Queries) TouchUser(ctx context.Context, pk int32) error {
|
||||
|
||||
const updateUser = `-- name: UpdateUser :one
|
||||
update
|
||||
users
|
||||
keibi.users
|
||||
set
|
||||
username = coalesce($2, username),
|
||||
email = coalesce($3, email),
|
||||
password = coalesce($4, password),
|
||||
claims = coalesce($5, claims)
|
||||
claims = claims || coalesce($5, '{}'::jsonb)
|
||||
where
|
||||
id = $1
|
||||
returning
|
||||
|
||||
18
auth/devspace.yaml
Normal file
18
auth/devspace.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: v2beta1
|
||||
name: auth
|
||||
dev:
|
||||
auth:
|
||||
imageSelector: ghcr.io/zoriya/kyoo_auth
|
||||
devImage: docker.io/golang:1.25
|
||||
workingDir: /app
|
||||
sync:
|
||||
- path: .:/app
|
||||
startContainer: true
|
||||
onUpload:
|
||||
restartContainer: true
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- "go mod download; go run -race ."
|
||||
ports:
|
||||
- port: "4568"
|
||||
58
auth/go.mod
58
auth/go.mod
@@ -1,36 +1,59 @@
|
||||
module github.com/zoriya/kyoo/keibi
|
||||
|
||||
go 1.23.3
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.25.1
|
||||
toolchain go1.25.4
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/exaring/otelpgx v0.9.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/labstack/echo-jwt/v4 v4.3.1
|
||||
github.com/labstack/echo-jwt/v4 v4.4.0
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.10
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12
|
||||
github.com/swaggo/echo-swagger v1.4.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
||||
go.opentelemetry.io/otel/log v0.14.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0 // indirect
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
google.golang.org/grpc v1.75.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
@@ -38,7 +61,7 @@ require (
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@@ -55,13 +78,16 @@ require (
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
135
auth/go.sum
135
auth/go.sum
@@ -6,6 +6,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
@@ -25,12 +27,15 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/exaring/otelpgx v0.9.3 h1:4yO02tXC7ZJZ+hcqcUkfxblYNCIFGVhpUWI0iw1TzPU=
|
||||
github.com/exaring/otelpgx v0.9.3/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
@@ -49,8 +54,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -59,10 +64,14 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -84,8 +93,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo-jwt/v4 v4.3.1 h1:d8+/qf8nx7RxeL46LtoIwHJsH2PNN8xXCQ/jDianycE=
|
||||
github.com/labstack/echo-jwt/v4 v4.3.1/go.mod h1:yJi83kN8S/5vePVPd+7ID75P4PqPNVRs2HVeuvYJH00=
|
||||
github.com/labstack/echo-jwt/v4 v4.4.0 h1:nrXaEnJupfc2R4XChcLRDyghhMZup77F8nIzHnBK19U=
|
||||
github.com/labstack/echo-jwt/v4 v4.4.0/go.mod h1:kYXWgWms9iFqI3ldR+HAEj/Zfg5rZtR7ePOgktG4Hjg=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
@@ -94,12 +103,16 @@ 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/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||
@@ -126,17 +139,17 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
|
||||
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
|
||||
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
||||
@@ -152,35 +165,67 @@ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 h1:6YeICKmGrvgJ5th4+OMNpcuoB6q/Xs8gt0YCO7MUv1k=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0/go.mod h1:ZEA7j2B35siNV0T00aapacNzjz4tvOlNoHp0ncCfwNQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -190,8 +235,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -203,17 +248,27 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
19
auth/jwt.go
19
auth/jwt.go
@@ -34,19 +34,30 @@ func (h *Handler) CreateJwt(c echo.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Header().Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
return c.JSON(http.StatusOK, Jwt{
|
||||
Token: &token,
|
||||
})
|
||||
}
|
||||
|
||||
auth := c.Request().Header.Get("Authorization")
|
||||
var jwt *string
|
||||
var token string
|
||||
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
if auth == "" {
|
||||
c, _ := c.Request().Cookie("X-Bearer")
|
||||
if c != nil {
|
||||
token = c.Value
|
||||
}
|
||||
} else if strings.HasPrefix(auth, "Bearer ") {
|
||||
token = auth[len("Bearer "):]
|
||||
} else if auth != "" {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid bearer format.")
|
||||
}
|
||||
|
||||
var jwt *string
|
||||
if token == "" {
|
||||
jwt = h.createGuestJwt()
|
||||
} else {
|
||||
token := auth[len("Bearer "):]
|
||||
|
||||
tkn, err := h.createJwt(token)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
62
auth/main.go
62
auth/main.go
@@ -19,10 +19,12 @@ import (
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/labstack/echo-jwt/v4"
|
||||
echojwt "github.com/labstack/echo-jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/swaggo/echo-swagger"
|
||||
echoSwagger "github.com/swaggo/echo-swagger"
|
||||
|
||||
"github.com/exaring/otelpgx"
|
||||
)
|
||||
|
||||
func ErrorHandler(err error, c echo.Context) {
|
||||
@@ -59,7 +61,27 @@ func (v *Validator) Validate(i any) error {
|
||||
}
|
||||
|
||||
func (h *Handler) CheckHealth(c echo.Context) error {
|
||||
return c.JSON(200, struct{ Status string }{Status: "healthy"})
|
||||
return c.JSON(200, struct {
|
||||
Status string `json:"status"`
|
||||
}{Status: "healthy"})
|
||||
}
|
||||
|
||||
func (h *Handler) CheckReady(c echo.Context) error {
|
||||
_, err := h.rawDb.Exec(c.Request().Context(), "select 1")
|
||||
|
||||
status := "healthy"
|
||||
db := "healthy"
|
||||
ret := 200
|
||||
if err != nil {
|
||||
status = "unhealthy"
|
||||
db = err.Error()
|
||||
ret = 500
|
||||
}
|
||||
|
||||
return c.JSON(ret, struct {
|
||||
Status string `json:"status"`
|
||||
Database string `json:"database"`
|
||||
}{Status: status, Database: db})
|
||||
}
|
||||
|
||||
func GetenvOr(env string, def string) string {
|
||||
@@ -106,10 +128,11 @@ func OpenDatabase() (*pgxpool.Pool, error) {
|
||||
config.ConnConfig.RuntimeParams["application_name"] = "keibi"
|
||||
}
|
||||
|
||||
schema := GetenvOr("POSTGRES_SCHEMA", "keibi")
|
||||
if _, ok := config.ConnConfig.RuntimeParams["search_path"]; !ok {
|
||||
config.ConnConfig.RuntimeParams["search_path"] = schema
|
||||
}
|
||||
config.ConnConfig.Tracer = otelpgx.NewTracer(
|
||||
otelpgx.WithSpanNameFunc(dbGetSpanName),
|
||||
otelpgx.WithDisableQuerySpanNamePrefix(),
|
||||
otelpgx.WithIncludeQueryParameters(),
|
||||
)
|
||||
|
||||
db, err := pgxpool.NewWithConfig(ctx, config)
|
||||
if err != nil {
|
||||
@@ -117,18 +140,14 @@ func OpenDatabase() (*pgxpool.Pool, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if schema != "disabled" {
|
||||
_, err = db.Exec(ctx, fmt.Sprintf("create schema if not exists %s", schema))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Migrating database")
|
||||
dbi := stdlib.OpenDBFromPool(db)
|
||||
defer dbi.Close()
|
||||
|
||||
driver, err := pgxd.WithInstance(dbi, &pgxd.Config{})
|
||||
dbi.Exec("create schema if not exists keibi")
|
||||
driver, err := pgxd.WithInstance(dbi, &pgxd.Config{
|
||||
SchemaName: "keibi",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -144,6 +163,7 @@ func OpenDatabase() (*pgxpool.Pool, error) {
|
||||
|
||||
type Handler struct {
|
||||
db *dbc.Queries
|
||||
rawDb *pgxpool.Pool
|
||||
config *Configuration
|
||||
}
|
||||
|
||||
@@ -212,6 +232,13 @@ func main() {
|
||||
e.Validator = &Validator{validator: validator.New(validator.WithRequiredStructEnabled())}
|
||||
e.HTTPErrorHandler = ErrorHandler
|
||||
|
||||
cleanup, err := setupOtel(e)
|
||||
if err != nil {
|
||||
e.Logger.Fatal("Failed to setup otel: ", err)
|
||||
return
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
db, err := OpenDatabase()
|
||||
if err != nil {
|
||||
e.Logger.Fatal("Could not open database: ", err)
|
||||
@@ -219,7 +246,8 @@ func main() {
|
||||
}
|
||||
|
||||
h := Handler{
|
||||
db: dbc.New(db),
|
||||
db: dbc.New(db),
|
||||
rawDb: db,
|
||||
}
|
||||
conf, err := LoadConfiguration(h.db)
|
||||
if err != nil {
|
||||
@@ -237,6 +265,7 @@ func main() {
|
||||
}))
|
||||
|
||||
g.GET("/health", h.CheckHealth)
|
||||
g.GET("/ready", h.CheckReady)
|
||||
|
||||
r.GET("/users", h.ListUsers)
|
||||
r.GET("/users/:id", h.GetUser)
|
||||
@@ -257,6 +286,7 @@ func main() {
|
||||
r.DELETE("/keys/:id", h.DeleteApiKey)
|
||||
|
||||
g.GET("/jwt", h.CreateJwt)
|
||||
g.Any("/jwt/*", h.CreateJwt)
|
||||
e.GET("/.well-known/jwks.json", h.GetJwks)
|
||||
e.GET("/.well-known/openid-configuration", h.GetOidcConfig)
|
||||
|
||||
|
||||
140
auth/otel.go
Normal file
140
auth/otel.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/log/global"
|
||||
"go.opentelemetry.io/otel/sdk/log"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
)
|
||||
|
||||
func setupOtel(e *echo.Echo) (func(), error) {
|
||||
ctx := context.Background()
|
||||
proto := GetenvOr("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf")
|
||||
|
||||
res, err := resource.New(
|
||||
ctx,
|
||||
resource.WithAttributes(semconv.ServiceNameKey.String("kyoo.auth")),
|
||||
resource.WithFromEnv(),
|
||||
resource.WithTelemetrySDK(),
|
||||
resource.WithProcess(),
|
||||
resource.WithOS(),
|
||||
resource.WithContainer(),
|
||||
resource.WithHost(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var le log.Exporter
|
||||
if proto == "http/protobuf" {
|
||||
le, err = otlploghttp.New(ctx)
|
||||
} else {
|
||||
le, err = otlploggrpc.New(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lp := log.NewLoggerProvider(
|
||||
log.WithProcessor(log.NewBatchProcessor(le)),
|
||||
log.WithResource(res),
|
||||
)
|
||||
global.SetLoggerProvider(lp)
|
||||
|
||||
var me metric.Exporter
|
||||
if proto == "http/protobuf" {
|
||||
me, err = otlpmetrichttp.New(ctx)
|
||||
} else {
|
||||
me, err = otlpmetricgrpc.New(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return func() {}, err
|
||||
}
|
||||
mp := metric.NewMeterProvider(
|
||||
metric.WithReader(
|
||||
metric.NewPeriodicReader(me),
|
||||
),
|
||||
metric.WithResource(res),
|
||||
)
|
||||
otel.SetMeterProvider(mp)
|
||||
|
||||
var te *otlptrace.Exporter
|
||||
if proto == "http/protobuf" {
|
||||
te, err = otlptracehttp.New(ctx)
|
||||
} else {
|
||||
te, err = otlptracegrpc.New(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return func() {}, err
|
||||
}
|
||||
tp := trace.NewTracerProvider(
|
||||
trace.WithBatcher(te),
|
||||
trace.WithResource(res),
|
||||
)
|
||||
otel.SetTracerProvider(tp)
|
||||
|
||||
e.Use(otelecho.Middleware("kyoo.auth", otelecho.WithSkipper(func(c echo.Context) bool {
|
||||
return c.Path() == "/auth/health" || c.Path() == "/auth/ready"
|
||||
})))
|
||||
|
||||
return func() {
|
||||
lp.Shutdown(ctx)
|
||||
mp.Shutdown(ctx)
|
||||
tp.Shutdown(ctx)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// stolen from https://github.com/exaring/otelpgx/issues/47
|
||||
func dbGetSpanName(sql string) string {
|
||||
if len(sql) >= 10 && sql[0:9] == "-- name: " {
|
||||
// -- name: {name} :{type}
|
||||
if index := strings.Index(sql[9:], ":"); index != -1 {
|
||||
// remove leading space before :
|
||||
// optimised assuming this comment has been generated by SQLC
|
||||
return sql[9 : 9+(index-1)]
|
||||
}
|
||||
// -- name: {name}
|
||||
if index := strings.Index(sql[9:], "\n"); index != -1 {
|
||||
if sql[len(sql[9:9+index])-1] != ' ' {
|
||||
return sql[9 : 9+index]
|
||||
}
|
||||
return strings.TrimSpace(sql[9 : 9+index])
|
||||
}
|
||||
}
|
||||
|
||||
if len(sql) >= 9 && sql[0:8] == "-- name:" {
|
||||
// -- name:{name}
|
||||
if index := strings.Index(sql[8:], "\n"); index != -1 {
|
||||
if sql[len(sql[8:8+index])-1] != ' ' {
|
||||
return sql[8 : 8+index]
|
||||
}
|
||||
return strings.TrimSpace(sql[8 : 8+index])
|
||||
}
|
||||
}
|
||||
|
||||
if len(sql) >= 8 && sql[0:7] == "--name:" {
|
||||
// -- name:{name}
|
||||
if index := strings.Index(sql[7:], "\n"); index != -1 {
|
||||
if sql[len(sql[7:7+index])-1] != ' ' {
|
||||
return sql[7 : 7+index]
|
||||
}
|
||||
return strings.TrimSpace(sql[7 : 7+index])
|
||||
}
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
@@ -7,9 +7,9 @@ pkgs.mkShell {
|
||||
sqlc
|
||||
go-swag
|
||||
# for psql in cli (+ pgformatter for sql files)
|
||||
postgresql_15
|
||||
postgresql_18
|
||||
pgformatter
|
||||
# to run tests
|
||||
# hurl
|
||||
hurl
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
begin;
|
||||
|
||||
drop table oidc_handle;
|
||||
drop table users;
|
||||
drop table keibi.oidc_handle;
|
||||
drop table keibi.users;
|
||||
|
||||
commit;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
begin;
|
||||
|
||||
create table users(
|
||||
create schema if not exists keibi;
|
||||
|
||||
create table keibi.users(
|
||||
pk serial primary key,
|
||||
id uuid not null default gen_random_uuid(),
|
||||
username varchar(256) not null unique,
|
||||
@@ -12,8 +14,8 @@ create table users(
|
||||
last_seen timestamptz not null default now()::timestamptz
|
||||
);
|
||||
|
||||
create table oidc_handle(
|
||||
user_pk integer not null references users(pk) on delete cascade,
|
||||
create table keibi.oidc_handle(
|
||||
user_pk integer not null references keibi.users(pk) on delete cascade,
|
||||
provider varchar(256) not null,
|
||||
|
||||
id text not null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
begin;
|
||||
|
||||
drop table sessions;
|
||||
drop table keibi.sessions;
|
||||
|
||||
commit;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
begin;
|
||||
|
||||
create table sessions(
|
||||
create table keibi.sessions(
|
||||
pk serial primary key,
|
||||
id uuid not null default gen_random_uuid(),
|
||||
token varchar(128) not null unique,
|
||||
user_pk integer not null references users(pk) on delete cascade,
|
||||
user_pk integer not null references keibi.users(pk) on delete cascade,
|
||||
created_date timestamptz not null default now()::timestamptz,
|
||||
last_used timestamptz not null default now()::timestamptz,
|
||||
device varchar(1024)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
begin;
|
||||
|
||||
drop table apikeys;
|
||||
drop table keibi.apikeys;
|
||||
|
||||
commit;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
begin;
|
||||
|
||||
create table apikeys(
|
||||
create table keibi.apikeys(
|
||||
pk serial primary key,
|
||||
id uuid not null default gen_random_uuid(),
|
||||
name varchar(256) not null unique,
|
||||
token varchar(128) not null unique,
|
||||
claims jsonb not null,
|
||||
|
||||
created_by integer references users(pk) on delete cascade,
|
||||
created_by integer references keibi.users(pk) on delete cascade,
|
||||
created_at timestamptz not null default now()::timestamptz,
|
||||
last_used timestamptz not null default now()::timestamptz
|
||||
);
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
select
|
||||
*
|
||||
from
|
||||
apikeys
|
||||
keibi.apikeys
|
||||
where
|
||||
name = $1
|
||||
and token = $2;
|
||||
token = $1;
|
||||
|
||||
-- name: TouchApiKey :exec
|
||||
update
|
||||
apikeys
|
||||
keibi.apikeys
|
||||
set
|
||||
last_used = now()::timestamptz
|
||||
where
|
||||
@@ -19,18 +18,18 @@ where
|
||||
select
|
||||
*
|
||||
from
|
||||
apikeys
|
||||
keibi.apikeys
|
||||
order by
|
||||
last_used;
|
||||
|
||||
-- name: CreateApiKey :one
|
||||
insert into apikeys(name, token, claims, created_by)
|
||||
insert into keibi.apikeys(name, token, claims, created_by)
|
||||
values ($1, $2, $3, $4)
|
||||
returning
|
||||
*;
|
||||
|
||||
-- name: DeleteApiKey :one
|
||||
delete from apikeys
|
||||
delete from keibi.apikeys
|
||||
where id = $1
|
||||
returning
|
||||
*;
|
||||
|
||||
@@ -5,15 +5,15 @@ select
|
||||
s.last_used,
|
||||
sqlc.embed(u)
|
||||
from
|
||||
users as u
|
||||
inner join sessions as s on u.pk = s.user_pk
|
||||
keibi.users as u
|
||||
inner join keibi.sessions as s on u.pk = s.user_pk
|
||||
where
|
||||
s.token = $1
|
||||
limit 1;
|
||||
|
||||
-- name: TouchSession :exec
|
||||
update
|
||||
sessions
|
||||
keibi.sessions
|
||||
set
|
||||
last_used = now()::timestamptz
|
||||
where
|
||||
@@ -23,21 +23,21 @@ where
|
||||
select
|
||||
s.*
|
||||
from
|
||||
sessions as s
|
||||
inner join users as u on u.pk = s.user_pk
|
||||
keibi.sessions as s
|
||||
inner join keibi.users as u on u.pk = s.user_pk
|
||||
where
|
||||
u.pk = $1
|
||||
order by
|
||||
last_used;
|
||||
|
||||
-- name: CreateSession :one
|
||||
insert into sessions(token, user_pk, device)
|
||||
insert into keibi.sessions(token, user_pk, device)
|
||||
values ($1, $2, $3)
|
||||
returning
|
||||
*;
|
||||
|
||||
-- name: DeleteSession :one
|
||||
delete from sessions as s using users as u
|
||||
delete from keibi.sessions as s using keibi.users as u
|
||||
where s.user_pk = u.pk
|
||||
and s.id = $1
|
||||
and u.id = sqlc.arg(user_id)
|
||||
@@ -45,7 +45,7 @@ returning
|
||||
s.*;
|
||||
|
||||
-- name: ClearOtherSessions :exec
|
||||
delete from sessions as s using users as u
|
||||
delete from keibi.sessions as s using keibi.users as u
|
||||
where s.user_pk = u.pk
|
||||
and s.id != @session_id
|
||||
and u.id = @user_id;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user