mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-08 02:34:16 -04:00
v5 api: initial schemas, types & basic routes (#680)
This commit is contained in:
commit
3aee4bd3e2
14
api/.env.example
Normal file
14
api/.env.example
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# vi: ft=sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
|
||||||
|
# either an hard-coded secret to decode jwts or empty to use keibi's public secret.
|
||||||
|
# this should only be used in tests
|
||||||
|
JWT_SECRET=
|
||||||
|
# keibi's server to retrive the public jwt secret
|
||||||
|
AUHT_SERVER=http://auth:4568
|
||||||
|
|
||||||
|
POSTGRES_USER=kyoo
|
||||||
|
POSTGRES_PASSWORD=password
|
||||||
|
POSTGRES_DB=kyooDB
|
||||||
|
POSTGRES_SERVER=postgres
|
||||||
|
POSTGRES_PORT=5432
|
2
api/.gitignore
vendored
Normal file
2
api/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
**/*.bun
|
@ -98,7 +98,7 @@ erDiagram
|
|||||||
datetime next_refresh
|
datetime next_refresh
|
||||||
jsonb external_id
|
jsonb external_id
|
||||||
}
|
}
|
||||||
|
|
||||||
season_translations {
|
season_translations {
|
||||||
guid id PK,FK
|
guid id PK,FK
|
||||||
string language PK
|
string language PK
|
||||||
@ -107,17 +107,17 @@ erDiagram
|
|||||||
jsonb poster
|
jsonb poster
|
||||||
jsonb banner
|
jsonb banner
|
||||||
jsonb logo
|
jsonb logo
|
||||||
jsonb thumbnail
|
jsonb thumbnail
|
||||||
}
|
}
|
||||||
seasons ||--|{ season_translations : has
|
seasons ||--|{ season_translations : has
|
||||||
seasons ||--o{ entries : has
|
seasons ||--o{ entries : has
|
||||||
shows ||--|{ seasons : has
|
shows ||--|{ seasons : has
|
||||||
|
|
||||||
watched_shows {
|
watched_shows {
|
||||||
guid show_id PK, FK
|
guid show_id PK, FK
|
||||||
guid user_id PK, FK
|
guid user_id PK, FK
|
||||||
status status "completed|watching|droped|planned"
|
status status "completed|watching|droped|planned"
|
||||||
uint seen_entry_count "NN"
|
uint seen_entry_count "NN"
|
||||||
}
|
}
|
||||||
shows ||--|{ watched_shows : has
|
shows ||--|{ watched_shows : has
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ erDiagram
|
|||||||
datetime played_date
|
datetime played_date
|
||||||
}
|
}
|
||||||
entries ||--|{ watched_entries : has
|
entries ||--|{ watched_entries : has
|
||||||
|
|
||||||
roles {
|
roles {
|
||||||
guid show_id PK, FK
|
guid show_id PK, FK
|
||||||
guid staff_id PK, FK
|
guid staff_id PK, FK
|
||||||
@ -152,7 +152,7 @@ erDiagram
|
|||||||
datetime next_refresh
|
datetime next_refresh
|
||||||
jsonb external_id
|
jsonb external_id
|
||||||
}
|
}
|
||||||
|
|
||||||
staff_translations {
|
staff_translations {
|
||||||
guid id PK,FK
|
guid id PK,FK
|
||||||
string language PK
|
string language PK
|
||||||
|
6
api/biome.json
Normal file
6
api/biome.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../biome.json"],
|
||||||
|
"formatter": {
|
||||||
|
"lineWidth": 80
|
||||||
|
}
|
||||||
|
}
|
237
api/bun.lock
Executable file
237
api/bun.lock
Executable file
@ -0,0 +1,237 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 0,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@elysiajs/jwt": "^1.2.0",
|
||||||
|
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||||
|
"drizzle-kit": "^0.30.1",
|
||||||
|
"drizzle-orm": "^0.38.3",
|
||||||
|
"elysia": "^1.2.10",
|
||||||
|
"parjs": "^1.3.9",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
|
"bun-types": "^1.1.42",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||||
|
|
||||||
|
"@elysiajs/jwt": ["@elysiajs/jwt@1.2.0", "", { "dependencies": { "jose": "^4.14.4" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-5iMoZucIKNAqPKW3n6RBIyCnDWG3kOcqA4WZKtqEff+IjV6AN3dlMSE2XsS0xjIvusLD0UBXS8cxQ9NwIcj6ew=="],
|
||||||
|
|
||||||
|
"@elysiajs/swagger": ["@elysiajs/swagger@github:zoriya/elysia-swagger#bb8047e", { "dependencies": { "@scalar/themes": "^0.9.58", "@scalar/types": "^0.0.25", "openapi-types": "^12.1.3", "pathe": "^2.0.0" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "zoriya-elysia-swagger-bb8047e"],
|
||||||
|
|
||||||
|
"@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.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
|
||||||
|
|
||||||
|
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.5", "", {}, "sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw=="],
|
||||||
|
|
||||||
|
"@scalar/themes": ["@scalar/themes@0.9.58", "", { "dependencies": { "@scalar/types": "0.0.25" } }, "sha512-voMgCIq0N19N8Ehjs8rSS0j5P1mpgWbpN5dXIToGUbVj7KcxMnOfkH3P1/cy2CoUd1gRYe0newUBEcI1+tQi1g=="],
|
||||||
|
|
||||||
|
"@scalar/types": ["@scalar/types@0.0.25", "", { "dependencies": { "@scalar/openapi-types": "0.1.5", "@unhead/schema": "^1.11.11" } }, "sha512-sGnOFnfiSn4o23rklU/jrg81hO+630bsFIdHg8MZ/w2Nc6IoUwARA2hbe4d4Fg+D0KBu40Tan/L+WAYDXkTJQg=="],
|
||||||
|
|
||||||
|
"@sinclair/typebox": ["@sinclair/typebox@0.34.13", "", {}, "sha512-ceVKqyCEgC355Kw0s/0tyfY9MzMQINSykJ/pG2w6YnaZyrcjV48svZpr8lVZrYgWjzOmrIPBhQRAtr/7eJpA5g=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.10.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ=="],
|
||||||
|
|
||||||
|
"@types/pg": ["@types/pg@8.11.10", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg=="],
|
||||||
|
|
||||||
|
"@types/ws": ["@types/ws@8.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA=="],
|
||||||
|
|
||||||
|
"@unhead/schema": ["@unhead/schema@1.11.14", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-V9W9u5tF1/+TiLqxu+Qvh1ShoMDkPEwHoEo4DKdDG6ko7YlbzFfDxV6el9JwCren45U/4Vy/4Xi7j8OH02wsiA=="],
|
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.1.42", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-beMbnFqWbbBQHll/bn3phSwmoOQmnX2nt8NI9iOQKFbgR5Z6rlH3YuaMdlid8vp5XGct3/W4QVQBmhoOEoe4nw=="],
|
||||||
|
|
||||||
|
"char-info": ["char-info@0.3.5", "", { "dependencies": { "node-interval-tree": "^1.3.3" } }, "sha512-gRslEBFEcuLMGLNO1EFIrdN1MMUfO+aqa7y8iWzNyAzB3mYKnTIvP+ioW3jpyeEvqA5WapVLIPINGtFjEIH4cQ=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||||
|
|
||||||
|
"drizzle-kit": ["drizzle-kit@0.30.1", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-HmA/NeewvHywhJ2ENXD3KvOuM/+K2dGLJfxVfIHsGwaqKICJnS+Ke2L6UcSrSrtMJLJaT0Im1Qv4TFXfaZShyw=="],
|
||||||
|
|
||||||
|
"drizzle-orm": ["drizzle-orm@0.38.3", "", { "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", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "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/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-w41Y+PquMpSff/QDRGdItG0/aWca+/J3Sda9PPGkTxBtjWQvgU1jxlFBXdjog5tYvTu58uvi3PwR1NuCx0KeZg=="],
|
||||||
|
|
||||||
|
"elysia": ["elysia@1.2.10", "", { "dependencies": { "@sinclair/typebox": "^0.34.13", "cookie": "^1.0.2", "memoirist": "^0.2.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-QcNl2FjhHFRpKaqy1NoMpyCjJ7OcKBnHwLUkqGu09QwIV84PFb82ILvYJG4GS1RbGv76OA50luaqBLrM3SLZ2w=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
|
||||||
|
|
||||||
|
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.8.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg=="],
|
||||||
|
|
||||||
|
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
|
"jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
||||||
|
|
||||||
|
"memoirist": ["memoirist@0.2.0", "", {}, "sha512-DA1V11OWsKmYjgYHfT1luus0FtTjUbILfI9s5M+ckK29tBLON6GDhH5GwxDz7E1ou4Bdzm9vhbeCaRAWxwG+0g=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.0", "", {}, "sha512-G7n4uhtk9qJt2hlD+UFfsIGY854wpF+zs2bUbQ3CQEUTcn7v25LRsrmurOxTo4bJgjE4qkyshd9ldsEuY9M6xg=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.13.1", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.7.0", "pg-protocol": "^1.7.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.7.0", "", {}, "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="],
|
||||||
|
|
||||||
|
"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.7.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.7.0", "", {}, "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@3.0.2", "", {}, "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
|
||||||
|
|
||||||
|
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
|
||||||
|
|
||||||
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||||
|
|
||||||
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
}
|
||||||
|
}
|
2
api/bunfig.toml
Normal file
2
api/bunfig.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[test]
|
||||||
|
preload = ["./tests/setup.ts"]
|
14
api/drizzle.config.ts
Normal file
14
api/drizzle.config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: "./drizzle",
|
||||||
|
schema: "./src/db/schema",
|
||||||
|
dialect: "postgresql",
|
||||||
|
casing: "snake_case",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
schema: "kyoo",
|
||||||
|
},
|
||||||
|
});
|
102
api/drizzle/0000_init.sql
Normal file
102
api/drizzle/0000_init.sql
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
CREATE TYPE "kyoo"."entry_type" AS ENUM('unknown', 'episode', 'movie', 'special', 'extra');--> statement-breakpoint
|
||||||
|
CREATE TYPE "kyoo"."genres" AS ENUM('action', 'adventure', 'animation', 'comedy', 'crime', 'documentary', 'drama', 'family', 'fantasy', 'history', 'horror', 'music', 'mystery', 'romance', 'science-fiction', 'thriller', 'war', 'western', 'kids', 'reality', 'politics', 'soap', 'talk');--> statement-breakpoint
|
||||||
|
CREATE TYPE "kyoo"."show_kind" AS ENUM('serie', 'movie');--> statement-breakpoint
|
||||||
|
CREATE TYPE "kyoo"."show_status" AS ENUM('unknown', 'finished', 'airing', 'planned');--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."entries" (
|
||||||
|
"pk" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "kyoo"."entries_pk_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"slug" varchar(255) NOT NULL,
|
||||||
|
"show_pk" integer,
|
||||||
|
"order" integer NOT NULL,
|
||||||
|
"season_number" integer,
|
||||||
|
"episode_number" integer,
|
||||||
|
"type" "kyoo"."entry_type" NOT NULL,
|
||||||
|
"air_date" date,
|
||||||
|
"runtime" integer,
|
||||||
|
"thumbnails" jsonb,
|
||||||
|
"external_id" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now(),
|
||||||
|
"next_refresh" timestamp with time zone,
|
||||||
|
CONSTRAINT "entries_id_unique" UNIQUE("id"),
|
||||||
|
CONSTRAINT "entries_slug_unique" UNIQUE("slug"),
|
||||||
|
CONSTRAINT "entries_showPk_seasonNumber_episodeNumber_unique" UNIQUE("show_pk","season_number","episode_number"),
|
||||||
|
CONSTRAINT "order_positive" CHECK ("entries"."order" >= 0)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."entries_translation" (
|
||||||
|
"pk" integer NOT NULL,
|
||||||
|
"language" varchar(255) NOT NULL,
|
||||||
|
"name" text,
|
||||||
|
"description" text,
|
||||||
|
CONSTRAINT "entries_translation_pk_language_pk" PRIMARY KEY("pk","language")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."show_translations" (
|
||||||
|
"pk" integer NOT NULL,
|
||||||
|
"language" varchar(255) NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"tagline" text,
|
||||||
|
"aliases" text[] NOT NULL,
|
||||||
|
"tags" text[] NOT NULL,
|
||||||
|
"trailer_url" text,
|
||||||
|
"poster" jsonb,
|
||||||
|
"thumbnail" jsonb,
|
||||||
|
"banner" jsonb,
|
||||||
|
"logo" jsonb,
|
||||||
|
CONSTRAINT "show_translations_pk_language_pk" PRIMARY KEY("pk","language")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."shows" (
|
||||||
|
"pk" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "kyoo"."shows_pk_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"slug" varchar(255) NOT NULL,
|
||||||
|
"kind" "kyoo"."show_kind" NOT NULL,
|
||||||
|
"genres" "kyoo"."genres"[] NOT NULL,
|
||||||
|
"rating" smallint,
|
||||||
|
"runtime" integer,
|
||||||
|
"status" "kyoo"."show_status" NOT NULL,
|
||||||
|
"start_air" date,
|
||||||
|
"end_air" date,
|
||||||
|
"original_language" varchar(255),
|
||||||
|
"external_id" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"next_refresh" timestamp with time zone NOT NULL,
|
||||||
|
CONSTRAINT "shows_id_unique" UNIQUE("id"),
|
||||||
|
CONSTRAINT "shows_slug_unique" UNIQUE("slug"),
|
||||||
|
CONSTRAINT "rating_valid" CHECK ("shows"."rating" between 0 and 100),
|
||||||
|
CONSTRAINT "runtime_valid" CHECK ("shows"."runtime" >= 0)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."videos" (
|
||||||
|
"pk" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "kyoo"."videos_pk_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"path" text NOT NULL,
|
||||||
|
"rendering" integer,
|
||||||
|
"part" integer,
|
||||||
|
"version" integer,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "videos_id_unique" UNIQUE("id"),
|
||||||
|
CONSTRAINT "videos_path_unique" UNIQUE("path"),
|
||||||
|
CONSTRAINT "rendering_pos" CHECK ("videos"."rendering" >= 0),
|
||||||
|
CONSTRAINT "part_pos" CHECK ("videos"."part" >= 0),
|
||||||
|
CONSTRAINT "version_pos" CHECK ("videos"."version" >= 0)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."entries" ADD CONSTRAINT "entries_show_pk_shows_pk_fk" FOREIGN KEY ("show_pk") REFERENCES "kyoo"."shows"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."entries_translation" ADD CONSTRAINT "entries_translation_pk_entries_pk_fk" FOREIGN KEY ("pk") REFERENCES "kyoo"."entries"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."show_translations" ADD CONSTRAINT "show_translations_pk_shows_pk_fk" FOREIGN KEY ("pk") REFERENCES "kyoo"."shows"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
7
api/drizzle/0001_video.sql
Normal file
7
api/drizzle/0001_video.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE "kyoo"."videos" DROP CONSTRAINT "rendering_pos";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ALTER COLUMN "rendering" SET DATA TYPE text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ALTER COLUMN "rendering" SET NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ALTER COLUMN "version" SET DEFAULT 1;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ALTER COLUMN "version" SET NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ADD COLUMN "slug" varchar(255) NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ADD CONSTRAINT "videos_slug_unique" UNIQUE("slug");
|
40
api/drizzle/0002_seasons.sql
Normal file
40
api/drizzle/0002_seasons.sql
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."season_translation" (
|
||||||
|
"pk" integer NOT NULL,
|
||||||
|
"language" varchar(255) NOT NULL,
|
||||||
|
"name" text,
|
||||||
|
"description" text,
|
||||||
|
"poster" jsonb,
|
||||||
|
"thumbnail" jsonb,
|
||||||
|
"logo" jsonb,
|
||||||
|
"banner" jsonb,
|
||||||
|
CONSTRAINT "season_translation_pk_language_pk" PRIMARY KEY("pk","language")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."seasons" (
|
||||||
|
"pk" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "kyoo"."seasons_pk_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"slug" varchar(255) NOT NULL,
|
||||||
|
"show_pk" integer,
|
||||||
|
"season_number" integer NOT NULL,
|
||||||
|
"start_air" date,
|
||||||
|
"end_air" date,
|
||||||
|
"external_id" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now(),
|
||||||
|
"next_refresh" timestamp with time zone,
|
||||||
|
CONSTRAINT "seasons_id_unique" UNIQUE("id"),
|
||||||
|
CONSTRAINT "seasons_slug_unique" UNIQUE("slug"),
|
||||||
|
CONSTRAINT "seasons_showPk_seasonNumber_unique" UNIQUE("show_pk","season_number")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entries" ALTER COLUMN "order" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."season_translation" ADD CONSTRAINT "season_translation_pk_seasons_pk_fk" FOREIGN KEY ("pk") REFERENCES "kyoo"."seasons"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."seasons" ADD CONSTRAINT "seasons_show_pk_shows_pk_fk" FOREIGN KEY ("show_pk") REFERENCES "kyoo"."shows"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
3
api/drizzle/0003_order.sql
Normal file
3
api/drizzle/0003_order.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "kyoo"."entries" ALTER COLUMN "order" SET DATA TYPE real;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entries_translation" ADD COLUMN "tagline" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."season_translation" DROP COLUMN IF EXISTS "logo";
|
56
api/drizzle/0004_jointures.sql
Normal file
56
api/drizzle/0004_jointures.sql
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "kyoo"."entry_video_jointure" (
|
||||||
|
"entry" integer NOT NULL,
|
||||||
|
"video" integer NOT NULL,
|
||||||
|
"slug" varchar(255) NOT NULL,
|
||||||
|
CONSTRAINT "entry_video_jointure_entry_video_pk" PRIMARY KEY("entry","video"),
|
||||||
|
CONSTRAINT "entry_video_jointure_slug_unique" UNIQUE("slug")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entries_translation" RENAME TO "entry_translations";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."season_translation" RENAME TO "season_translations";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" DROP CONSTRAINT "videos_slug_unique";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entries" DROP CONSTRAINT "order_positive";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."shows" DROP CONSTRAINT "rating_valid";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."shows" DROP CONSTRAINT "runtime_valid";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" DROP CONSTRAINT "part_pos";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" DROP CONSTRAINT "version_pos";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entry_translations" DROP CONSTRAINT "entries_translation_pk_entries_pk_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."season_translations" DROP CONSTRAINT "season_translation_pk_seasons_pk_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entry_translations" DROP CONSTRAINT "entries_translation_pk_language_pk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."season_translations" DROP CONSTRAINT "season_translation_pk_language_pk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entry_translations" ADD CONSTRAINT "entry_translations_pk_language_pk" PRIMARY KEY("pk","language");--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."season_translations" ADD CONSTRAINT "season_translations_pk_language_pk" PRIMARY KEY("pk","language");--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entry_translations" ADD COLUMN "poster" jsonb;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ADD COLUMN "guess" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."entry_video_jointure" ADD CONSTRAINT "entry_video_jointure_entry_entries_pk_fk" FOREIGN KEY ("entry") REFERENCES "kyoo"."entries"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."entry_video_jointure" ADD CONSTRAINT "entry_video_jointure_video_videos_pk_fk" FOREIGN KEY ("video") REFERENCES "kyoo"."videos"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."entry_translations" ADD CONSTRAINT "entry_translations_pk_entries_pk_fk" FOREIGN KEY ("pk") REFERENCES "kyoo"."entries"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "kyoo"."season_translations" ADD CONSTRAINT "season_translations_pk_seasons_pk_fk" FOREIGN KEY ("pk") REFERENCES "kyoo"."seasons"("pk") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" DROP COLUMN IF EXISTS "slug";--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."entries" ADD CONSTRAINT "order_positive" CHECK ("kyoo"."entries"."order" >= 0);--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."shows" ADD CONSTRAINT "rating_valid" CHECK ("kyoo"."shows"."rating" between 0 and 100);--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."shows" ADD CONSTRAINT "runtime_valid" CHECK ("kyoo"."shows"."runtime" >= 0);--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ADD CONSTRAINT "part_pos" CHECK ("kyoo"."videos"."part" >= 0);--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."videos" ADD CONSTRAINT "version_pos" CHECK ("kyoo"."videos"."version" >= 0);
|
589
api/drizzle/meta/0000_snapshot.json
Normal file
589
api/drizzle/meta/0000_snapshot.json
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
{
|
||||||
|
"id": "82560792-5f4a-4723-9543-808719ade682",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"kyoo.entries": {
|
||||||
|
"name": "entries",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "entries_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"episode_number": {
|
||||||
|
"name": "episode_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "entry_type",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"air_date": {
|
||||||
|
"name": "air_date",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"name": "thumbnails",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_show_pk_shows_pk_fk": {
|
||||||
|
"name": "entries_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "entries",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"entries_id_unique": {
|
||||||
|
"name": "entries_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"entries_slug_unique": {
|
||||||
|
"name": "entries_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"entries_showPk_seasonNumber_episodeNumber_unique": {
|
||||||
|
"name": "entries_showPk_seasonNumber_episodeNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number", "episode_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"order_positive": {
|
||||||
|
"name": "order_positive",
|
||||||
|
"value": "\"entries\".\"order\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.entries_translation": {
|
||||||
|
"name": "entries_translation",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_translation_pk_entries_pk_fk": {
|
||||||
|
"name": "entries_translation_pk_entries_pk_fk",
|
||||||
|
"tableFrom": "entries_translation",
|
||||||
|
"tableTo": "entries",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"entries_translation_pk_language_pk": {
|
||||||
|
"name": "entries_translation_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.show_translations": {
|
||||||
|
"name": "show_translations",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tagline": {
|
||||||
|
"name": "tagline",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"name": "aliases",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"trailer_url": {
|
||||||
|
"name": "trailer_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"name": "logo",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"show_translations_pk_shows_pk_fk": {
|
||||||
|
"name": "show_translations_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "show_translations",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"show_translations_pk_language_pk": {
|
||||||
|
"name": "show_translations_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.shows": {
|
||||||
|
"name": "shows",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "shows_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"name": "kind",
|
||||||
|
"type": "show_kind",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"type": "genres[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "smallint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "show_status",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"original_language": {
|
||||||
|
"name": "original_language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"shows_id_unique": {
|
||||||
|
"name": "shows_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"shows_slug_unique": {
|
||||||
|
"name": "shows_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"rating_valid": {
|
||||||
|
"name": "rating_valid",
|
||||||
|
"value": "\"shows\".\"rating\" between 0 and 100"
|
||||||
|
},
|
||||||
|
"runtime_valid": {
|
||||||
|
"name": "runtime_valid",
|
||||||
|
"value": "\"shows\".\"runtime\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.videos": {
|
||||||
|
"name": "videos",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "videos_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rendering": {
|
||||||
|
"name": "rendering",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"part": {
|
||||||
|
"name": "part",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"videos_id_unique": {
|
||||||
|
"name": "videos_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"videos_path_unique": {
|
||||||
|
"name": "videos_path_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["path"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"rendering_pos": {
|
||||||
|
"name": "rendering_pos",
|
||||||
|
"value": "\"videos\".\"rendering\" >= 0"
|
||||||
|
},
|
||||||
|
"part_pos": {
|
||||||
|
"name": "part_pos",
|
||||||
|
"value": "\"videos\".\"part\" >= 0"
|
||||||
|
},
|
||||||
|
"version_pos": {
|
||||||
|
"name": "version_pos",
|
||||||
|
"value": "\"videos\".\"version\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"kyoo.entry_type": {
|
||||||
|
"name": "entry_type",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "episode", "movie", "special", "extra"]
|
||||||
|
},
|
||||||
|
"kyoo.genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": [
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"animation",
|
||||||
|
"comedy",
|
||||||
|
"crime",
|
||||||
|
"documentary",
|
||||||
|
"drama",
|
||||||
|
"family",
|
||||||
|
"fantasy",
|
||||||
|
"history",
|
||||||
|
"horror",
|
||||||
|
"music",
|
||||||
|
"mystery",
|
||||||
|
"romance",
|
||||||
|
"science-fiction",
|
||||||
|
"thriller",
|
||||||
|
"war",
|
||||||
|
"western",
|
||||||
|
"kids",
|
||||||
|
"reality",
|
||||||
|
"politics",
|
||||||
|
"soap",
|
||||||
|
"talk"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kyoo.show_kind": {
|
||||||
|
"name": "show_kind",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["serie", "movie"]
|
||||||
|
},
|
||||||
|
"kyoo.show_status": {
|
||||||
|
"name": "show_status",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "finished", "airing", "planned"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"kyoo": "kyoo"
|
||||||
|
},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
597
api/drizzle/meta/0001_snapshot.json
Normal file
597
api/drizzle/meta/0001_snapshot.json
Normal file
@ -0,0 +1,597 @@
|
|||||||
|
{
|
||||||
|
"id": "32090852-33a7-430a-9df1-97608c063124",
|
||||||
|
"prevId": "82560792-5f4a-4723-9543-808719ade682",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"kyoo.entries": {
|
||||||
|
"name": "entries",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "entries_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"episode_number": {
|
||||||
|
"name": "episode_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "entry_type",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"air_date": {
|
||||||
|
"name": "air_date",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"name": "thumbnails",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_show_pk_shows_pk_fk": {
|
||||||
|
"name": "entries_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "entries",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"entries_id_unique": {
|
||||||
|
"name": "entries_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"entries_slug_unique": {
|
||||||
|
"name": "entries_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"entries_showPk_seasonNumber_episodeNumber_unique": {
|
||||||
|
"name": "entries_showPk_seasonNumber_episodeNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number", "episode_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"order_positive": {
|
||||||
|
"name": "order_positive",
|
||||||
|
"value": "\"entries\".\"order\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.entries_translation": {
|
||||||
|
"name": "entries_translation",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_translation_pk_entries_pk_fk": {
|
||||||
|
"name": "entries_translation_pk_entries_pk_fk",
|
||||||
|
"tableFrom": "entries_translation",
|
||||||
|
"tableTo": "entries",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"entries_translation_pk_language_pk": {
|
||||||
|
"name": "entries_translation_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.show_translations": {
|
||||||
|
"name": "show_translations",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tagline": {
|
||||||
|
"name": "tagline",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"name": "aliases",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"trailer_url": {
|
||||||
|
"name": "trailer_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"name": "logo",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"show_translations_pk_shows_pk_fk": {
|
||||||
|
"name": "show_translations_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "show_translations",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"show_translations_pk_language_pk": {
|
||||||
|
"name": "show_translations_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.shows": {
|
||||||
|
"name": "shows",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "shows_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"name": "kind",
|
||||||
|
"type": "show_kind",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"type": "genres[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "smallint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "show_status",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"original_language": {
|
||||||
|
"name": "original_language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"shows_id_unique": {
|
||||||
|
"name": "shows_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"shows_slug_unique": {
|
||||||
|
"name": "shows_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"rating_valid": {
|
||||||
|
"name": "rating_valid",
|
||||||
|
"value": "\"shows\".\"rating\" between 0 and 100"
|
||||||
|
},
|
||||||
|
"runtime_valid": {
|
||||||
|
"name": "runtime_valid",
|
||||||
|
"value": "\"shows\".\"runtime\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.videos": {
|
||||||
|
"name": "videos",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "videos_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rendering": {
|
||||||
|
"name": "rendering",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"part": {
|
||||||
|
"name": "part",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"videos_id_unique": {
|
||||||
|
"name": "videos_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"videos_slug_unique": {
|
||||||
|
"name": "videos_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"videos_path_unique": {
|
||||||
|
"name": "videos_path_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["path"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"part_pos": {
|
||||||
|
"name": "part_pos",
|
||||||
|
"value": "\"videos\".\"part\" >= 0"
|
||||||
|
},
|
||||||
|
"version_pos": {
|
||||||
|
"name": "version_pos",
|
||||||
|
"value": "\"videos\".\"version\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"kyoo.entry_type": {
|
||||||
|
"name": "entry_type",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "episode", "movie", "special", "extra"]
|
||||||
|
},
|
||||||
|
"kyoo.genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": [
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"animation",
|
||||||
|
"comedy",
|
||||||
|
"crime",
|
||||||
|
"documentary",
|
||||||
|
"drama",
|
||||||
|
"family",
|
||||||
|
"fantasy",
|
||||||
|
"history",
|
||||||
|
"horror",
|
||||||
|
"music",
|
||||||
|
"mystery",
|
||||||
|
"romance",
|
||||||
|
"science-fiction",
|
||||||
|
"thriller",
|
||||||
|
"war",
|
||||||
|
"western",
|
||||||
|
"kids",
|
||||||
|
"reality",
|
||||||
|
"politics",
|
||||||
|
"soap",
|
||||||
|
"talk"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kyoo.show_kind": {
|
||||||
|
"name": "show_kind",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["serie", "movie"]
|
||||||
|
},
|
||||||
|
"kyoo.show_status": {
|
||||||
|
"name": "show_status",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "finished", "airing", "planned"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"kyoo": "kyoo"
|
||||||
|
},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
788
api/drizzle/meta/0002_snapshot.json
Normal file
788
api/drizzle/meta/0002_snapshot.json
Normal file
@ -0,0 +1,788 @@
|
|||||||
|
{
|
||||||
|
"id": "d0f6c500-aa2b-4592-aa31-db646817f708",
|
||||||
|
"prevId": "32090852-33a7-430a-9df1-97608c063124",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"kyoo.entries": {
|
||||||
|
"name": "entries",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "entries_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"episode_number": {
|
||||||
|
"name": "episode_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "entry_type",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"air_date": {
|
||||||
|
"name": "air_date",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"name": "thumbnails",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_show_pk_shows_pk_fk": {
|
||||||
|
"name": "entries_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "entries",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"entries_id_unique": {
|
||||||
|
"name": "entries_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"entries_slug_unique": {
|
||||||
|
"name": "entries_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"entries_showPk_seasonNumber_episodeNumber_unique": {
|
||||||
|
"name": "entries_showPk_seasonNumber_episodeNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number", "episode_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"order_positive": {
|
||||||
|
"name": "order_positive",
|
||||||
|
"value": "\"entries\".\"order\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.entries_translation": {
|
||||||
|
"name": "entries_translation",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_translation_pk_entries_pk_fk": {
|
||||||
|
"name": "entries_translation_pk_entries_pk_fk",
|
||||||
|
"tableFrom": "entries_translation",
|
||||||
|
"tableTo": "entries",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"entries_translation_pk_language_pk": {
|
||||||
|
"name": "entries_translation_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.season_translation": {
|
||||||
|
"name": "season_translation",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"name": "logo",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"season_translation_pk_seasons_pk_fk": {
|
||||||
|
"name": "season_translation_pk_seasons_pk_fk",
|
||||||
|
"tableFrom": "season_translation",
|
||||||
|
"tableTo": "seasons",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"season_translation_pk_language_pk": {
|
||||||
|
"name": "season_translation_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.seasons": {
|
||||||
|
"name": "seasons",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "seasons_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"seasons_show_pk_shows_pk_fk": {
|
||||||
|
"name": "seasons_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "seasons",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"seasons_id_unique": {
|
||||||
|
"name": "seasons_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"seasons_slug_unique": {
|
||||||
|
"name": "seasons_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"seasons_showPk_seasonNumber_unique": {
|
||||||
|
"name": "seasons_showPk_seasonNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.show_translations": {
|
||||||
|
"name": "show_translations",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tagline": {
|
||||||
|
"name": "tagline",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"name": "aliases",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"trailer_url": {
|
||||||
|
"name": "trailer_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"name": "logo",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"show_translations_pk_shows_pk_fk": {
|
||||||
|
"name": "show_translations_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "show_translations",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"show_translations_pk_language_pk": {
|
||||||
|
"name": "show_translations_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.shows": {
|
||||||
|
"name": "shows",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "shows_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"name": "kind",
|
||||||
|
"type": "show_kind",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"type": "genres[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "smallint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "show_status",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"original_language": {
|
||||||
|
"name": "original_language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"shows_id_unique": {
|
||||||
|
"name": "shows_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"shows_slug_unique": {
|
||||||
|
"name": "shows_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"rating_valid": {
|
||||||
|
"name": "rating_valid",
|
||||||
|
"value": "\"shows\".\"rating\" between 0 and 100"
|
||||||
|
},
|
||||||
|
"runtime_valid": {
|
||||||
|
"name": "runtime_valid",
|
||||||
|
"value": "\"shows\".\"runtime\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.videos": {
|
||||||
|
"name": "videos",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "videos_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rendering": {
|
||||||
|
"name": "rendering",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"part": {
|
||||||
|
"name": "part",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"videos_id_unique": {
|
||||||
|
"name": "videos_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"videos_slug_unique": {
|
||||||
|
"name": "videos_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"videos_path_unique": {
|
||||||
|
"name": "videos_path_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["path"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"part_pos": {
|
||||||
|
"name": "part_pos",
|
||||||
|
"value": "\"videos\".\"part\" >= 0"
|
||||||
|
},
|
||||||
|
"version_pos": {
|
||||||
|
"name": "version_pos",
|
||||||
|
"value": "\"videos\".\"version\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"kyoo.entry_type": {
|
||||||
|
"name": "entry_type",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "episode", "movie", "special", "extra"]
|
||||||
|
},
|
||||||
|
"kyoo.genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": [
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"animation",
|
||||||
|
"comedy",
|
||||||
|
"crime",
|
||||||
|
"documentary",
|
||||||
|
"drama",
|
||||||
|
"family",
|
||||||
|
"fantasy",
|
||||||
|
"history",
|
||||||
|
"horror",
|
||||||
|
"music",
|
||||||
|
"mystery",
|
||||||
|
"romance",
|
||||||
|
"science-fiction",
|
||||||
|
"thriller",
|
||||||
|
"war",
|
||||||
|
"western",
|
||||||
|
"kids",
|
||||||
|
"reality",
|
||||||
|
"politics",
|
||||||
|
"soap",
|
||||||
|
"talk"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kyoo.show_kind": {
|
||||||
|
"name": "show_kind",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["serie", "movie"]
|
||||||
|
},
|
||||||
|
"kyoo.show_status": {
|
||||||
|
"name": "show_status",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "finished", "airing", "planned"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"kyoo": "kyoo"
|
||||||
|
},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
788
api/drizzle/meta/0003_snapshot.json
Normal file
788
api/drizzle/meta/0003_snapshot.json
Normal file
@ -0,0 +1,788 @@
|
|||||||
|
{
|
||||||
|
"id": "2210fd60-8e6a-4503-a2b3-56cc7f3cf15a",
|
||||||
|
"prevId": "d0f6c500-aa2b-4592-aa31-db646817f708",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"kyoo.entries": {
|
||||||
|
"name": "entries",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "entries_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"episode_number": {
|
||||||
|
"name": "episode_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "entry_type",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"air_date": {
|
||||||
|
"name": "air_date",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"name": "thumbnails",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_show_pk_shows_pk_fk": {
|
||||||
|
"name": "entries_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "entries",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"entries_id_unique": {
|
||||||
|
"name": "entries_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"entries_slug_unique": {
|
||||||
|
"name": "entries_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"entries_showPk_seasonNumber_episodeNumber_unique": {
|
||||||
|
"name": "entries_showPk_seasonNumber_episodeNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number", "episode_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"order_positive": {
|
||||||
|
"name": "order_positive",
|
||||||
|
"value": "\"entries\".\"order\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.entries_translation": {
|
||||||
|
"name": "entries_translation",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tagline": {
|
||||||
|
"name": "tagline",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_translation_pk_entries_pk_fk": {
|
||||||
|
"name": "entries_translation_pk_entries_pk_fk",
|
||||||
|
"tableFrom": "entries_translation",
|
||||||
|
"tableTo": "entries",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"entries_translation_pk_language_pk": {
|
||||||
|
"name": "entries_translation_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.season_translation": {
|
||||||
|
"name": "season_translation",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"season_translation_pk_seasons_pk_fk": {
|
||||||
|
"name": "season_translation_pk_seasons_pk_fk",
|
||||||
|
"tableFrom": "season_translation",
|
||||||
|
"tableTo": "seasons",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"season_translation_pk_language_pk": {
|
||||||
|
"name": "season_translation_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.seasons": {
|
||||||
|
"name": "seasons",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "seasons_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"seasons_show_pk_shows_pk_fk": {
|
||||||
|
"name": "seasons_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "seasons",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"seasons_id_unique": {
|
||||||
|
"name": "seasons_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"seasons_slug_unique": {
|
||||||
|
"name": "seasons_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"seasons_showPk_seasonNumber_unique": {
|
||||||
|
"name": "seasons_showPk_seasonNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.show_translations": {
|
||||||
|
"name": "show_translations",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tagline": {
|
||||||
|
"name": "tagline",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"name": "aliases",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"trailer_url": {
|
||||||
|
"name": "trailer_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"name": "logo",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"show_translations_pk_shows_pk_fk": {
|
||||||
|
"name": "show_translations_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "show_translations",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"show_translations_pk_language_pk": {
|
||||||
|
"name": "show_translations_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.shows": {
|
||||||
|
"name": "shows",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "shows_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"name": "kind",
|
||||||
|
"type": "show_kind",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"type": "genres[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "smallint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "show_status",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"original_language": {
|
||||||
|
"name": "original_language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"shows_id_unique": {
|
||||||
|
"name": "shows_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"shows_slug_unique": {
|
||||||
|
"name": "shows_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"rating_valid": {
|
||||||
|
"name": "rating_valid",
|
||||||
|
"value": "\"shows\".\"rating\" between 0 and 100"
|
||||||
|
},
|
||||||
|
"runtime_valid": {
|
||||||
|
"name": "runtime_valid",
|
||||||
|
"value": "\"shows\".\"runtime\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.videos": {
|
||||||
|
"name": "videos",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "videos_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rendering": {
|
||||||
|
"name": "rendering",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"part": {
|
||||||
|
"name": "part",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"videos_id_unique": {
|
||||||
|
"name": "videos_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"videos_slug_unique": {
|
||||||
|
"name": "videos_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"videos_path_unique": {
|
||||||
|
"name": "videos_path_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["path"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"part_pos": {
|
||||||
|
"name": "part_pos",
|
||||||
|
"value": "\"videos\".\"part\" >= 0"
|
||||||
|
},
|
||||||
|
"version_pos": {
|
||||||
|
"name": "version_pos",
|
||||||
|
"value": "\"videos\".\"version\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"kyoo.entry_type": {
|
||||||
|
"name": "entry_type",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "episode", "movie", "special", "extra"]
|
||||||
|
},
|
||||||
|
"kyoo.genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": [
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"animation",
|
||||||
|
"comedy",
|
||||||
|
"crime",
|
||||||
|
"documentary",
|
||||||
|
"drama",
|
||||||
|
"family",
|
||||||
|
"fantasy",
|
||||||
|
"history",
|
||||||
|
"horror",
|
||||||
|
"music",
|
||||||
|
"mystery",
|
||||||
|
"romance",
|
||||||
|
"science-fiction",
|
||||||
|
"thriller",
|
||||||
|
"war",
|
||||||
|
"western",
|
||||||
|
"kids",
|
||||||
|
"reality",
|
||||||
|
"politics",
|
||||||
|
"soap",
|
||||||
|
"talk"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kyoo.show_kind": {
|
||||||
|
"name": "show_kind",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["serie", "movie"]
|
||||||
|
},
|
||||||
|
"kyoo.show_status": {
|
||||||
|
"name": "show_status",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "finished", "airing", "planned"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"kyoo": "kyoo"
|
||||||
|
},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
853
api/drizzle/meta/0004_snapshot.json
Normal file
853
api/drizzle/meta/0004_snapshot.json
Normal file
@ -0,0 +1,853 @@
|
|||||||
|
{
|
||||||
|
"id": "0d5d6d22-dc13-4f3d-9975-cb7b38f628d4",
|
||||||
|
"prevId": "2210fd60-8e6a-4503-a2b3-56cc7f3cf15a",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"kyoo.entries": {
|
||||||
|
"name": "entries",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "entries_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"episode_number": {
|
||||||
|
"name": "episode_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "entry_type",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"air_date": {
|
||||||
|
"name": "air_date",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"name": "thumbnails",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entries_show_pk_shows_pk_fk": {
|
||||||
|
"name": "entries_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "entries",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"entries_id_unique": {
|
||||||
|
"name": "entries_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"entries_slug_unique": {
|
||||||
|
"name": "entries_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"entries_showPk_seasonNumber_episodeNumber_unique": {
|
||||||
|
"name": "entries_showPk_seasonNumber_episodeNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number", "episode_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"order_positive": {
|
||||||
|
"name": "order_positive",
|
||||||
|
"value": "\"kyoo\".\"entries\".\"order\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.entry_translations": {
|
||||||
|
"name": "entry_translations",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tagline": {
|
||||||
|
"name": "tagline",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entry_translations_pk_entries_pk_fk": {
|
||||||
|
"name": "entry_translations_pk_entries_pk_fk",
|
||||||
|
"tableFrom": "entry_translations",
|
||||||
|
"tableTo": "entries",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"entry_translations_pk_language_pk": {
|
||||||
|
"name": "entry_translations_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.season_translations": {
|
||||||
|
"name": "season_translations",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"season_translations_pk_seasons_pk_fk": {
|
||||||
|
"name": "season_translations_pk_seasons_pk_fk",
|
||||||
|
"tableFrom": "season_translations",
|
||||||
|
"tableTo": "seasons",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"season_translations_pk_language_pk": {
|
||||||
|
"name": "season_translations_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.seasons": {
|
||||||
|
"name": "seasons",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "seasons_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"show_pk": {
|
||||||
|
"name": "show_pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"season_number": {
|
||||||
|
"name": "season_number",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"seasons_show_pk_shows_pk_fk": {
|
||||||
|
"name": "seasons_show_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "seasons",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["show_pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"seasons_id_unique": {
|
||||||
|
"name": "seasons_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"seasons_slug_unique": {
|
||||||
|
"name": "seasons_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
},
|
||||||
|
"seasons_showPk_seasonNumber_unique": {
|
||||||
|
"name": "seasons_showPk_seasonNumber_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["show_pk", "season_number"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.show_translations": {
|
||||||
|
"name": "show_translations",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tagline": {
|
||||||
|
"name": "tagline",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"name": "aliases",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"name": "banner",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"name": "logo",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"trailer_url": {
|
||||||
|
"name": "trailer_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"show_translations_pk_shows_pk_fk": {
|
||||||
|
"name": "show_translations_pk_shows_pk_fk",
|
||||||
|
"tableFrom": "show_translations",
|
||||||
|
"tableTo": "shows",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["pk"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"show_translations_pk_language_pk": {
|
||||||
|
"name": "show_translations_pk_language_pk",
|
||||||
|
"columns": ["pk", "language"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.shows": {
|
||||||
|
"name": "shows",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "shows_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"name": "kind",
|
||||||
|
"type": "show_kind",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"type": "genres[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "smallint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "runtime",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "show_status",
|
||||||
|
"typeSchema": "kyoo",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"start_air": {
|
||||||
|
"name": "start_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"end_air": {
|
||||||
|
"name": "end_air",
|
||||||
|
"type": "date",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"original_language": {
|
||||||
|
"name": "original_language",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"external_id": {
|
||||||
|
"name": "external_id",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"next_refresh": {
|
||||||
|
"name": "next_refresh",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"shows_id_unique": {
|
||||||
|
"name": "shows_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"shows_slug_unique": {
|
||||||
|
"name": "shows_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"rating_valid": {
|
||||||
|
"name": "rating_valid",
|
||||||
|
"value": "\"kyoo\".\"shows\".\"rating\" between 0 and 100"
|
||||||
|
},
|
||||||
|
"runtime_valid": {
|
||||||
|
"name": "runtime_valid",
|
||||||
|
"value": "\"kyoo\".\"shows\".\"runtime\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.entry_video_jointure": {
|
||||||
|
"name": "entry_video_jointure",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"entry": {
|
||||||
|
"name": "entry",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"name": "video",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"entry_video_jointure_entry_entries_pk_fk": {
|
||||||
|
"name": "entry_video_jointure_entry_entries_pk_fk",
|
||||||
|
"tableFrom": "entry_video_jointure",
|
||||||
|
"tableTo": "entries",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["entry"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"entry_video_jointure_video_videos_pk_fk": {
|
||||||
|
"name": "entry_video_jointure_video_videos_pk_fk",
|
||||||
|
"tableFrom": "entry_video_jointure",
|
||||||
|
"tableTo": "videos",
|
||||||
|
"schemaTo": "kyoo",
|
||||||
|
"columnsFrom": ["video"],
|
||||||
|
"columnsTo": ["pk"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"entry_video_jointure_entry_video_pk": {
|
||||||
|
"name": "entry_video_jointure_entry_video_pk",
|
||||||
|
"columns": ["entry", "video"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"entry_video_jointure_slug_unique": {
|
||||||
|
"name": "entry_video_jointure_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["slug"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"kyoo.videos": {
|
||||||
|
"name": "videos",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"columns": {
|
||||||
|
"pk": {
|
||||||
|
"name": "pk",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"identity": {
|
||||||
|
"type": "always",
|
||||||
|
"name": "videos_pk_seq",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"increment": "1",
|
||||||
|
"startWith": "1",
|
||||||
|
"minValue": "1",
|
||||||
|
"maxValue": "2147483647",
|
||||||
|
"cache": "1",
|
||||||
|
"cycle": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"rendering": {
|
||||||
|
"name": "rendering",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"part": {
|
||||||
|
"name": "part",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"guess": {
|
||||||
|
"name": "guess",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"videos_id_unique": {
|
||||||
|
"name": "videos_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["id"]
|
||||||
|
},
|
||||||
|
"videos_path_unique": {
|
||||||
|
"name": "videos_path_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["path"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"part_pos": {
|
||||||
|
"name": "part_pos",
|
||||||
|
"value": "\"kyoo\".\"videos\".\"part\" >= 0"
|
||||||
|
},
|
||||||
|
"version_pos": {
|
||||||
|
"name": "version_pos",
|
||||||
|
"value": "\"kyoo\".\"videos\".\"version\" >= 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"kyoo.entry_type": {
|
||||||
|
"name": "entry_type",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "episode", "movie", "special", "extra"]
|
||||||
|
},
|
||||||
|
"kyoo.genres": {
|
||||||
|
"name": "genres",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": [
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"animation",
|
||||||
|
"comedy",
|
||||||
|
"crime",
|
||||||
|
"documentary",
|
||||||
|
"drama",
|
||||||
|
"family",
|
||||||
|
"fantasy",
|
||||||
|
"history",
|
||||||
|
"horror",
|
||||||
|
"music",
|
||||||
|
"mystery",
|
||||||
|
"romance",
|
||||||
|
"science-fiction",
|
||||||
|
"thriller",
|
||||||
|
"war",
|
||||||
|
"western",
|
||||||
|
"kids",
|
||||||
|
"reality",
|
||||||
|
"politics",
|
||||||
|
"soap",
|
||||||
|
"talk"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kyoo.show_kind": {
|
||||||
|
"name": "show_kind",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["serie", "movie"]
|
||||||
|
},
|
||||||
|
"kyoo.show_status": {
|
||||||
|
"name": "show_status",
|
||||||
|
"schema": "kyoo",
|
||||||
|
"values": ["unknown", "finished", "airing", "planned"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"kyoo": "kyoo"
|
||||||
|
},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
41
api/drizzle/meta/_journal.json
Normal file
41
api/drizzle/meta/_journal.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1731105746157,
|
||||||
|
"tag": "0000_init",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1731149082556,
|
||||||
|
"tag": "0001_video",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1731165599920,
|
||||||
|
"tag": "0002_seasons",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1731258712255,
|
||||||
|
"tag": "0003_order",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1732738409330,
|
||||||
|
"tag": "0004_jointures",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
24
api/package.json
Normal file
24
api/package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"version": "1.0.50",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --watch src/index.ts",
|
||||||
|
"build": "bun build src/index.ts --target bun --outdir ./dist",
|
||||||
|
"start": "NODE_ENV=production bun dist/index.js",
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@elysiajs/jwt": "^1.2.0",
|
||||||
|
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||||
|
"drizzle-kit": "^0.30.1",
|
||||||
|
"drizzle-orm": "^0.38.3",
|
||||||
|
"elysia": "^1.2.10",
|
||||||
|
"parjs": "^1.3.9",
|
||||||
|
"pg": "^8.13.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
|
"bun-types": "^1.1.42"
|
||||||
|
},
|
||||||
|
"module": "src/index.js"
|
||||||
|
}
|
27
api/src/base.ts
Normal file
27
api/src/base.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Elysia from "elysia";
|
||||||
|
import type { KError } from "./models/error";
|
||||||
|
|
||||||
|
export const base = new Elysia({ name: "base" })
|
||||||
|
.onError(({ code, error }) => {
|
||||||
|
if (code === "VALIDATION") {
|
||||||
|
const details = JSON.parse(error.message);
|
||||||
|
if (details.code === "KError") {
|
||||||
|
delete details.code;
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: error.status,
|
||||||
|
message: `Validation error on ${details.on}.`,
|
||||||
|
details: details,
|
||||||
|
} as KError;
|
||||||
|
}
|
||||||
|
if (code === "INTERNAL_SERVER_ERROR") {
|
||||||
|
console.error(error);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
message: error.message,
|
||||||
|
details: error,
|
||||||
|
} as KError;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.as("plugin");
|
32
api/src/controllers/entries.ts
Normal file
32
api/src/controllers/entries.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import {
|
||||||
|
type Entry,
|
||||||
|
Episode,
|
||||||
|
Extra,
|
||||||
|
MovieEntry,
|
||||||
|
Special,
|
||||||
|
UnknownEntry,
|
||||||
|
} from "../models/entry";
|
||||||
|
|
||||||
|
export const entries = new Elysia()
|
||||||
|
.model({
|
||||||
|
episode: Episode,
|
||||||
|
movie_entry: MovieEntry,
|
||||||
|
special: Special,
|
||||||
|
extra: Extra,
|
||||||
|
unknown_entry: UnknownEntry,
|
||||||
|
error: t.Object({}),
|
||||||
|
})
|
||||||
|
.model((models) => ({
|
||||||
|
...models,
|
||||||
|
entry: t.Union([models.episode, models.movie_entry, models.special]),
|
||||||
|
}))
|
||||||
|
.get("/entries/:id", () => "hello" as unknown as Entry, {
|
||||||
|
response: { 200: "entry" },
|
||||||
|
})
|
||||||
|
.get("/extras/:id", () => "hello" as unknown as Extra, {
|
||||||
|
response: { 200: "extra" },
|
||||||
|
})
|
||||||
|
.get("/unknowns/:id", () => "hello" as unknown as UnknownEntry, {
|
||||||
|
response: { 200: "unknown_entry" },
|
||||||
|
});
|
244
api/src/controllers/movies.ts
Normal file
244
api/src/controllers/movies.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import { and, desc, eq, sql } from "drizzle-orm";
|
||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { KError } from "~/models/error";
|
||||||
|
import { comment } from "~/utils";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { shows, showTranslations } from "../db/schema/shows";
|
||||||
|
import { getColumns } from "../db/schema/utils";
|
||||||
|
import { bubble } from "../models/examples";
|
||||||
|
import { Movie, MovieStatus, MovieTranslation } from "../models/movie";
|
||||||
|
import {
|
||||||
|
Filter,
|
||||||
|
Sort,
|
||||||
|
type FilterDef,
|
||||||
|
Genre,
|
||||||
|
isUuid,
|
||||||
|
keysetPaginate,
|
||||||
|
Page,
|
||||||
|
processLanguages,
|
||||||
|
createPage,
|
||||||
|
} from "~/models/utils";
|
||||||
|
|
||||||
|
// 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(",")}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTranslationQuery = (languages: string[], forceFallback = false) => {
|
||||||
|
const fallback = forceFallback || languages.includes("*");
|
||||||
|
const query = db
|
||||||
|
.selectDistinctOn([showTranslations.pk])
|
||||||
|
.from(showTranslations)
|
||||||
|
.where(
|
||||||
|
fallback
|
||||||
|
? undefined
|
||||||
|
: eq(showTranslations.language, sql`any(${sqlarr(languages)})`),
|
||||||
|
)
|
||||||
|
.orderBy(
|
||||||
|
showTranslations.pk,
|
||||||
|
sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
|
||||||
|
)
|
||||||
|
.as("t");
|
||||||
|
|
||||||
|
const { pk, ...col } = getColumns(query);
|
||||||
|
return [query, col] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// we keep the pk for after handling. it will be removed by elysia's validators after.
|
||||||
|
const { kind, startAir, endAir, ...moviesCol } = getColumns(shows);
|
||||||
|
|
||||||
|
const movieFilters: FilterDef = {
|
||||||
|
genres: {
|
||||||
|
column: shows.genres,
|
||||||
|
type: "enum",
|
||||||
|
values: Genre.enum,
|
||||||
|
isArray: true,
|
||||||
|
},
|
||||||
|
rating: { column: shows.rating, type: "int" },
|
||||||
|
status: { column: shows.status, type: "enum", values: MovieStatus.enum },
|
||||||
|
runtime: { column: shows.runtime, type: "float" },
|
||||||
|
airDate: { column: shows.startAir, type: "date" },
|
||||||
|
originalLanguage: { column: shows.originalLanguage, type: "string" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||||
|
.model({
|
||||||
|
movie: Movie,
|
||||||
|
"movie-translation": MovieTranslation,
|
||||||
|
})
|
||||||
|
.get(
|
||||||
|
"/:id",
|
||||||
|
async ({
|
||||||
|
params: { id },
|
||||||
|
headers: { "accept-language": languages },
|
||||||
|
error,
|
||||||
|
set,
|
||||||
|
}) => {
|
||||||
|
const langs = processLanguages(languages);
|
||||||
|
const [transQ, transCol] = getTranslationQuery(langs);
|
||||||
|
|
||||||
|
const idFilter = isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id);
|
||||||
|
|
||||||
|
const [ret] = await db
|
||||||
|
.select({
|
||||||
|
...moviesCol,
|
||||||
|
status: sql<MovieStatus>`${moviesCol.status}`,
|
||||||
|
airDate: startAir,
|
||||||
|
translation: transCol,
|
||||||
|
})
|
||||||
|
.from(shows)
|
||||||
|
.leftJoin(transQ, eq(shows.pk, transQ.pk))
|
||||||
|
.where(and(eq(shows.kind, "movie"), idFilter))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!ret) {
|
||||||
|
return error(404, {
|
||||||
|
status: 404,
|
||||||
|
message: "Movie not found",
|
||||||
|
details: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!ret.translation) {
|
||||||
|
return error(422, {
|
||||||
|
status: 422,
|
||||||
|
message: "Accept-Language header could not be satisfied.",
|
||||||
|
details: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
set.headers["content-language"] = ret.translation.language;
|
||||||
|
return { ...ret, ...ret.translation };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
description: "Get a movie by id or slug",
|
||||||
|
},
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String({
|
||||||
|
description: "The id or slug of the movie to retrieve.",
|
||||||
|
example: bubble.slug,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
headers: t.Object({
|
||||||
|
"accept-language": t.String({
|
||||||
|
default: "*",
|
||||||
|
example: "en-us, ja;q=0.5",
|
||||||
|
description: comment`
|
||||||
|
List of languages you want the data in.
|
||||||
|
This follows the [Accept-Language offical specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language).
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: { ...Movie, description: "Found" },
|
||||||
|
404: {
|
||||||
|
...KError,
|
||||||
|
description: "No movie found with the given id or slug.",
|
||||||
|
examples: [
|
||||||
|
{ status: 404, message: "Movie not found", details: undefined },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
422: {
|
||||||
|
...KError,
|
||||||
|
description: comment`
|
||||||
|
The Accept-Language header can't be satisfied (all languages listed are
|
||||||
|
unavailable.) Try with another languages or add * to the list of languages
|
||||||
|
to fallback to any language.
|
||||||
|
`,
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
status: 422,
|
||||||
|
message: "Accept-Language header could not be satisfied.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"",
|
||||||
|
async ({
|
||||||
|
query: { limit, after, sort, filter },
|
||||||
|
headers: { "accept-language": languages },
|
||||||
|
request: { url },
|
||||||
|
}) => {
|
||||||
|
const langs = processLanguages(languages);
|
||||||
|
const [transQ, transCol] = getTranslationQuery(langs, true);
|
||||||
|
|
||||||
|
// TODO: Add sql indexes on sort keys
|
||||||
|
|
||||||
|
const items = await db
|
||||||
|
.select({
|
||||||
|
...moviesCol,
|
||||||
|
...transCol,
|
||||||
|
status: sql<MovieStatus>`${moviesCol.status}`,
|
||||||
|
airDate: startAir,
|
||||||
|
})
|
||||||
|
.from(shows)
|
||||||
|
.innerJoin(transQ, eq(shows.pk, transQ.pk))
|
||||||
|
.where(and(filter, keysetPaginate({ table: shows, after, sort })))
|
||||||
|
.orderBy(
|
||||||
|
...sort.map((x) => (x.desc ? sql`${shows[x.key]} desc nulls last` : shows[x.key])),
|
||||||
|
shows.pk,
|
||||||
|
)
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
return createPage(items, { url, sort, limit });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: { description: "Get all movies" },
|
||||||
|
query: t.Object({
|
||||||
|
sort: Sort(["slug", "rating", "airDate", "createdAt", "nextRefresh"], {
|
||||||
|
// TODO: Add random
|
||||||
|
remap: { airDate: "startAir" },
|
||||||
|
default: ["slug"],
|
||||||
|
description: "How to sort the query",
|
||||||
|
}),
|
||||||
|
filter: t.Optional(Filter({ def: movieFilters })),
|
||||||
|
limit: t.Integer({
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 250,
|
||||||
|
default: 50,
|
||||||
|
description: "Max page size.",
|
||||||
|
}),
|
||||||
|
after: t.Optional(
|
||||||
|
t.String({
|
||||||
|
description: comment`
|
||||||
|
Id of the cursor in the pagination.
|
||||||
|
You can ignore this and only use the prev/next field in the response.
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
headers: t.Object({
|
||||||
|
"accept-language": t.String({
|
||||||
|
default: "*",
|
||||||
|
example: "en-us, ja;q=0.5",
|
||||||
|
description: comment`
|
||||||
|
List of languages you want the data in.
|
||||||
|
This follows the [Accept-Language offical specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language).
|
||||||
|
|
||||||
|
In this request, * is always implied (if no language could satisfy the request, kyoo will use any language available.)
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: Page(Movie, {
|
||||||
|
description: "Paginated list of movies that match filters.",
|
||||||
|
}),
|
||||||
|
422: {
|
||||||
|
...KError,
|
||||||
|
description: "Invalid query parameters.",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
status: 422,
|
||||||
|
message:
|
||||||
|
"Invalid property: slug. Expected one of genres, rating, status, runtime, airDate, originalLanguage.",
|
||||||
|
details: {
|
||||||
|
in: "slug eq bubble",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
11
api/src/controllers/seasons.ts
Normal file
11
api/src/controllers/seasons.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { Season } from "../models/season";
|
||||||
|
|
||||||
|
export const seasons = new Elysia({ prefix: "/seasons" })
|
||||||
|
.model({
|
||||||
|
season: Season,
|
||||||
|
error: t.Object({}),
|
||||||
|
})
|
||||||
|
.get("/:id", () => "hello" as unknown as Season, {
|
||||||
|
response: { 200: "season" },
|
||||||
|
});
|
19
api/src/controllers/seed/images.ts
Normal file
19
api/src/controllers/seed/images.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { Image } from "~/models/utils";
|
||||||
|
|
||||||
|
export const processImage = async (url: string): Promise<Image> => {
|
||||||
|
const hasher = new Bun.CryptoHasher("sha256");
|
||||||
|
hasher.update(url);
|
||||||
|
|
||||||
|
// TODO: download source, save it in multiples qualities & process blurhash
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: hasher.digest().toString("hex"),
|
||||||
|
source: url,
|
||||||
|
blurhash: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processOptImage = (url: string | null): Promise<Image | null> => {
|
||||||
|
if (!url) return Promise.resolve(null);
|
||||||
|
return processImage(url);
|
||||||
|
};
|
47
api/src/controllers/seed/index.ts
Normal file
47
api/src/controllers/seed/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import Elysia from "elysia";
|
||||||
|
import { Movie, SeedMovie } from "~/models/movie";
|
||||||
|
import { seedMovie, SeedMovieResponse } from "./movies";
|
||||||
|
import { Resource, validateTranslations } from "~/models/utils";
|
||||||
|
import { comment } from "~/utils";
|
||||||
|
import { KError } from "~/models/error";
|
||||||
|
|
||||||
|
export const seed = new Elysia()
|
||||||
|
.model({
|
||||||
|
movie: Movie,
|
||||||
|
"seed-movie": SeedMovie,
|
||||||
|
"seed-movie-response": SeedMovieResponse,
|
||||||
|
})
|
||||||
|
.post(
|
||||||
|
"/movies",
|
||||||
|
async ({ body, error }) => {
|
||||||
|
const err = validateTranslations(body.translations);
|
||||||
|
if (err) return error(400, err);
|
||||||
|
|
||||||
|
const { status, ...ret } = await seedMovie(body);
|
||||||
|
return error(status, ret);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: "seed-movie",
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
...SeedMovieResponse,
|
||||||
|
description: "Existing movie edited/updated.",
|
||||||
|
},
|
||||||
|
201: { ...SeedMovieResponse, description: "Created a new movie." },
|
||||||
|
400: { ...KError, description: "Invalid translation name" },
|
||||||
|
409: {
|
||||||
|
...Resource,
|
||||||
|
description: comment`
|
||||||
|
A movie with the same slug but a different air date already exists.
|
||||||
|
Change the slug and re-run the request.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
422: { ...KError, description: "Invalid schema in body." },
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
tags: ["movies"],
|
||||||
|
description:
|
||||||
|
"Create a movie & all related metadata. Can also link videos.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
183
api/src/controllers/seed/movies.ts
Normal file
183
api/src/controllers/seed/movies.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { inArray, sql, eq } from "drizzle-orm";
|
||||||
|
import { t } from "elysia";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import {
|
||||||
|
entries,
|
||||||
|
entryTranslations,
|
||||||
|
entryVideoJointure as evj,
|
||||||
|
shows,
|
||||||
|
showTranslations,
|
||||||
|
videos,
|
||||||
|
} from "~/db/schema";
|
||||||
|
import { conflictUpdateAllExcept } from "~/db/schema/utils";
|
||||||
|
import type { SeedMovie } from "~/models/movie";
|
||||||
|
import { processOptImage } from "./images";
|
||||||
|
import { guessNextRefresh } from "./refresh";
|
||||||
|
|
||||||
|
type Show = typeof shows.$inferInsert;
|
||||||
|
type ShowTrans = typeof showTranslations.$inferInsert;
|
||||||
|
type Entry = typeof entries.$inferInsert;
|
||||||
|
|
||||||
|
export const SeedMovieResponse = t.Object({
|
||||||
|
id: t.String({ format: "uuid" }),
|
||||||
|
slug: t.String({ format: "slug", examples: ["bubble"] }),
|
||||||
|
videos: t.Array(
|
||||||
|
t.Object({ slug: t.String({ format: "slug", examples: ["bubble-v2"] }) }),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
export type SeedMovieResponse = typeof SeedMovieResponse.static;
|
||||||
|
|
||||||
|
export const seedMovie = async (
|
||||||
|
seed: SeedMovie,
|
||||||
|
): Promise<
|
||||||
|
SeedMovieResponse & { status: "Created" | "OK" | "Conflict" }
|
||||||
|
> => {
|
||||||
|
const { translations, videos: vids, ...bMovie } = seed;
|
||||||
|
|
||||||
|
const ret = await db.transaction(async (tx) => {
|
||||||
|
const movie: Show = {
|
||||||
|
kind: "movie",
|
||||||
|
startAir: bMovie.airDate,
|
||||||
|
nextRefresh: guessNextRefresh(bMovie.airDate ?? new Date()),
|
||||||
|
...bMovie,
|
||||||
|
};
|
||||||
|
|
||||||
|
const insert = () =>
|
||||||
|
tx
|
||||||
|
.insert(shows)
|
||||||
|
.values(movie)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: shows.slug,
|
||||||
|
set: conflictUpdateAllExcept(shows, [
|
||||||
|
"pk",
|
||||||
|
"id",
|
||||||
|
"slug",
|
||||||
|
"createdAt",
|
||||||
|
]),
|
||||||
|
// if year is different, this is not an update but a conflict (ex: dune-1984 vs dune-2021)
|
||||||
|
setWhere: sql`date_part('year', ${shows.startAir}) = date_part('year', excluded."start_air")`,
|
||||||
|
})
|
||||||
|
.returning({
|
||||||
|
pk: shows.pk,
|
||||||
|
id: shows.id,
|
||||||
|
slug: shows.slug,
|
||||||
|
// https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns/39204667#39204667
|
||||||
|
updated: sql<boolean>`(xmax <> 0)`.as("updated"),
|
||||||
|
});
|
||||||
|
let [ret] = await insert();
|
||||||
|
if (!ret) {
|
||||||
|
// ret is undefined when the conflict's where return false (meaning we have
|
||||||
|
// a conflicting slug but a different air year.
|
||||||
|
// try to insert adding the year at the end of the slug.
|
||||||
|
if (
|
||||||
|
movie.startAir &&
|
||||||
|
!movie.slug.endsWith(`${getYear(movie.startAir)}`)
|
||||||
|
) {
|
||||||
|
movie.slug = `${movie.slug}-${getYear(movie.startAir)}`;
|
||||||
|
[ret] = await insert();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if at this point ret is still undefined, we could not reconciliate.
|
||||||
|
// simply bail and let the caller handle this.
|
||||||
|
if (!ret) {
|
||||||
|
const [{ id }] = await db
|
||||||
|
.select({ id: shows.id })
|
||||||
|
.from(shows)
|
||||||
|
.where(eq(shows.slug, movie.slug))
|
||||||
|
.limit(1);
|
||||||
|
return {
|
||||||
|
status: "Conflict" as const,
|
||||||
|
id,
|
||||||
|
slug: movie.slug,
|
||||||
|
videos: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// even if never shown to the user, a movie still has an entry.
|
||||||
|
const movieEntry: Entry = { type: "movie", ...bMovie };
|
||||||
|
const [entry] = await tx
|
||||||
|
.insert(entries)
|
||||||
|
.values(movieEntry)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: entries.slug,
|
||||||
|
set: conflictUpdateAllExcept(entries, [
|
||||||
|
"pk",
|
||||||
|
"id",
|
||||||
|
"slug",
|
||||||
|
"createdAt",
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
.returning({ pk: entries.pk });
|
||||||
|
|
||||||
|
const trans: ShowTrans[] = await Promise.all(
|
||||||
|
Object.entries(translations).map(async ([lang, tr]) => ({
|
||||||
|
pk: ret.pk,
|
||||||
|
// TODO: normalize lang or error if invalid
|
||||||
|
language: lang,
|
||||||
|
...tr,
|
||||||
|
poster: await processOptImage(tr.poster),
|
||||||
|
thumbnail: await processOptImage(tr.thumbnail),
|
||||||
|
logo: await processOptImage(tr.logo),
|
||||||
|
banner: await processOptImage(tr.banner),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
await tx
|
||||||
|
.insert(showTranslations)
|
||||||
|
.values(trans)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [showTranslations.pk, showTranslations.language],
|
||||||
|
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const entryTrans = trans.map((x) => ({ ...x, pk: entry.pk }));
|
||||||
|
await tx
|
||||||
|
.insert(entryTranslations)
|
||||||
|
.values(entryTrans)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [entryTranslations.pk, entryTranslations.language],
|
||||||
|
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...ret, entry: entry.pk };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ret.status === "Conflict") return ret;
|
||||||
|
|
||||||
|
let retVideos: { slug: string }[] = [];
|
||||||
|
if (vids) {
|
||||||
|
retVideos = await db
|
||||||
|
.insert(evj)
|
||||||
|
.select(
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
entry: sql<number>`${ret.entry}`.as("entry"),
|
||||||
|
video: videos.pk,
|
||||||
|
// TODO: do not add rendering if all videos of the entry have the same rendering
|
||||||
|
slug: sql<string>`
|
||||||
|
concat(
|
||||||
|
${ret.slug}::text,
|
||||||
|
case when ${videos.part} <> null then concat('-p', ${videos.part}) else '' end,
|
||||||
|
case when ${videos.version} <> 1 then concat('-v', ${videos.version}) else '' end
|
||||||
|
)
|
||||||
|
`.as("slug"),
|
||||||
|
// case when (select count(1) from ${evj} where ${evj.entry} = ${ret.entry}) <> 0 then concat('-', ${videos.rendering}) else '' end
|
||||||
|
})
|
||||||
|
.from(videos)
|
||||||
|
.where(inArray(videos.id, vids)),
|
||||||
|
)
|
||||||
|
.onConflictDoNothing()
|
||||||
|
.returning({ slug: evj.slug });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: ret.updated ? "OK" : "Created",
|
||||||
|
id: ret.id,
|
||||||
|
slug: ret.slug,
|
||||||
|
videos: retVideos,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getYear(date: string) {
|
||||||
|
return new Date(date).getUTCFullYear();
|
||||||
|
}
|
12
api/src/controllers/seed/refresh.ts
Normal file
12
api/src/controllers/seed/refresh.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// oh i hate js dates so much.
|
||||||
|
export const guessNextRefresh = (airDate: Date | string) => {
|
||||||
|
if (typeof airDate === "string") airDate = new Date(airDate);
|
||||||
|
const diff = new Date().getTime() - airDate.getTime();
|
||||||
|
const days = diff / (24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const ret = new Date();
|
||||||
|
if (days <= 4) ret.setDate(ret.getDate() + 4);
|
||||||
|
else if (days <= 21) ret.setDate(ret.getDate() + 14);
|
||||||
|
else ret.setMonth(ret.getMonth() + 2);
|
||||||
|
return ret.toISOString().substring(0, 10);
|
||||||
|
};
|
11
api/src/controllers/series.ts
Normal file
11
api/src/controllers/series.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { Serie } from "../models/serie";
|
||||||
|
|
||||||
|
export const series = new Elysia({ prefix: "/series" })
|
||||||
|
.model({
|
||||||
|
serie: Serie,
|
||||||
|
error: t.Object({}),
|
||||||
|
})
|
||||||
|
.get("/:id", () => "hello" as unknown as Serie, {
|
||||||
|
response: { 200: "serie" },
|
||||||
|
});
|
41
api/src/controllers/videos.ts
Normal file
41
api/src/controllers/videos.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { SeedVideo, Video } from "~/models/video";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { videos as videosT } from "~/db/schema";
|
||||||
|
import { comment } from "~/utils";
|
||||||
|
import { bubbleVideo } from "~/models/examples";
|
||||||
|
|
||||||
|
const CreatedVideo = t.Object({
|
||||||
|
id: t.String({ format: "uuid" }),
|
||||||
|
path: t.String({ example: bubbleVideo.path }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const videos = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||||
|
.model({
|
||||||
|
video: Video,
|
||||||
|
"created-videos": t.Array(CreatedVideo),
|
||||||
|
error: t.Object({}),
|
||||||
|
})
|
||||||
|
.get("/:id", () => "hello" as unknown as Video, {
|
||||||
|
response: { 200: "video" },
|
||||||
|
})
|
||||||
|
.post(
|
||||||
|
"/",
|
||||||
|
async ({ body }) => {
|
||||||
|
return await db
|
||||||
|
.insert(videosT)
|
||||||
|
.values(body)
|
||||||
|
.onConflictDoNothing()
|
||||||
|
.returning({ id: videosT.id, path: videosT.path });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Array(SeedVideo),
|
||||||
|
response: { 201: "created-videos" },
|
||||||
|
detail: {
|
||||||
|
description: comment`
|
||||||
|
Create videos in bulk.
|
||||||
|
Duplicated videos will simply be ignored.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
15
api/src/db/index.ts
Normal file
15
api/src/db/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
|
||||||
|
export const db = drizzle({
|
||||||
|
schema,
|
||||||
|
connection: {
|
||||||
|
user: process.env.POSTGRES_USER ?? "kyoo",
|
||||||
|
password: process.env.POSTGRES_PASSWORD ?? "password",
|
||||||
|
database: process.env.POSTGRES_DB ?? "kyooDB",
|
||||||
|
host: process.env.POSTGRES_SERVER ?? "postgres",
|
||||||
|
port: Number(process.env.POSTGRES_PORT) || 5432,
|
||||||
|
ssl: false,
|
||||||
|
},
|
||||||
|
casing: "snake_case",
|
||||||
|
});
|
88
api/src/db/schema/entries.ts
Normal file
88
api/src/db/schema/entries.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
check,
|
||||||
|
date,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
primaryKey,
|
||||||
|
real,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
unique,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { image, language, schema } from "./utils";
|
||||||
|
import { shows } from "./shows";
|
||||||
|
|
||||||
|
export const entryType = schema.enum("entry_type", [
|
||||||
|
"unknown",
|
||||||
|
"episode",
|
||||||
|
"movie",
|
||||||
|
"special",
|
||||||
|
"extra",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const entry_extid = () =>
|
||||||
|
jsonb()
|
||||||
|
.$type<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
| {
|
||||||
|
// used for movies
|
||||||
|
dataId: string;
|
||||||
|
link: string | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// used for episodes, specials & extra
|
||||||
|
serieId: string;
|
||||||
|
season: number | null;
|
||||||
|
episode: number;
|
||||||
|
link: string | null;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>()
|
||||||
|
.notNull()
|
||||||
|
.default({});
|
||||||
|
|
||||||
|
export const entries = schema.table(
|
||||||
|
"entries",
|
||||||
|
{
|
||||||
|
pk: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
id: uuid().notNull().unique().defaultRandom(),
|
||||||
|
slug: varchar({ length: 255 }).notNull().unique(),
|
||||||
|
showPk: integer().references(() => shows.pk, { onDelete: "cascade" }),
|
||||||
|
order: real(),
|
||||||
|
seasonNumber: integer(),
|
||||||
|
episodeNumber: integer(),
|
||||||
|
type: entryType().notNull(),
|
||||||
|
airDate: date(),
|
||||||
|
runtime: integer(),
|
||||||
|
thumbnails: image(),
|
||||||
|
|
||||||
|
externalId: entry_extid(),
|
||||||
|
|
||||||
|
createdAt: timestamp({ withTimezone: true, mode: "string" }).defaultNow(),
|
||||||
|
nextRefresh: timestamp({ withTimezone: true, mode: "string" }),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
unique().on(t.showPk, t.seasonNumber, t.episodeNumber),
|
||||||
|
check("order_positive", sql`${t.order} >= 0`),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const entryTranslations = schema.table(
|
||||||
|
"entry_translations",
|
||||||
|
{
|
||||||
|
pk: integer()
|
||||||
|
.notNull()
|
||||||
|
.references(() => entries.pk, { onDelete: "cascade" }),
|
||||||
|
language: language().notNull(),
|
||||||
|
name: text(),
|
||||||
|
description: text(),
|
||||||
|
// those two are only used if kind === "movie"
|
||||||
|
tagline: text(),
|
||||||
|
poster: image(),
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.pk, t.language] })],
|
||||||
|
);
|
4
api/src/db/schema/index.ts
Normal file
4
api/src/db/schema/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./entries";
|
||||||
|
export * from "./seasons";
|
||||||
|
export * from "./shows";
|
||||||
|
export * from "./videos";
|
63
api/src/db/schema/seasons.ts
Normal file
63
api/src/db/schema/seasons.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
date,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
primaryKey,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
unique,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { image, language, schema } from "./utils";
|
||||||
|
import { shows } from "./shows";
|
||||||
|
|
||||||
|
export const season_extid = () =>
|
||||||
|
jsonb()
|
||||||
|
.$type<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
serieId: string;
|
||||||
|
season: number;
|
||||||
|
link: string | null;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>()
|
||||||
|
.notNull()
|
||||||
|
.default({});
|
||||||
|
|
||||||
|
export const seasons = schema.table(
|
||||||
|
"seasons",
|
||||||
|
{
|
||||||
|
pk: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
id: uuid().notNull().unique().defaultRandom(),
|
||||||
|
slug: varchar({ length: 255 }).notNull().unique(),
|
||||||
|
showPk: integer().references(() => shows.pk, { onDelete: "cascade" }),
|
||||||
|
seasonNumber: integer().notNull(),
|
||||||
|
startAir: date(),
|
||||||
|
endAir: date(),
|
||||||
|
|
||||||
|
externalId: season_extid(),
|
||||||
|
|
||||||
|
createdAt: timestamp({ withTimezone: true, mode: "string" }).defaultNow(),
|
||||||
|
nextRefresh: timestamp({ withTimezone: true, mode: "string" }),
|
||||||
|
},
|
||||||
|
(t) => [unique().on(t.showPk, t.seasonNumber)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const seasonTranslation = schema.table(
|
||||||
|
"season_translations",
|
||||||
|
{
|
||||||
|
pk: integer()
|
||||||
|
.notNull()
|
||||||
|
.references(() => seasons.pk, { onDelete: "cascade" }),
|
||||||
|
language: language().notNull(),
|
||||||
|
name: text(),
|
||||||
|
description: text(),
|
||||||
|
poster: image(),
|
||||||
|
thumbnail: image(),
|
||||||
|
banner: image(),
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.pk, t.language] })],
|
||||||
|
);
|
121
api/src/db/schema/shows.ts
Normal file
121
api/src/db/schema/shows.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { relations, sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
check,
|
||||||
|
date,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
primaryKey,
|
||||||
|
smallint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { image, language, schema } from "./utils";
|
||||||
|
|
||||||
|
export const showKind = schema.enum("show_kind", ["serie", "movie"]);
|
||||||
|
export const showStatus = schema.enum("show_status", [
|
||||||
|
"unknown",
|
||||||
|
"finished",
|
||||||
|
"airing",
|
||||||
|
"planned",
|
||||||
|
]);
|
||||||
|
export const genres = schema.enum("genres", [
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"animation",
|
||||||
|
"comedy",
|
||||||
|
"crime",
|
||||||
|
"documentary",
|
||||||
|
"drama",
|
||||||
|
"family",
|
||||||
|
"fantasy",
|
||||||
|
"history",
|
||||||
|
"horror",
|
||||||
|
"music",
|
||||||
|
"mystery",
|
||||||
|
"romance",
|
||||||
|
"science-fiction",
|
||||||
|
"thriller",
|
||||||
|
"war",
|
||||||
|
"western",
|
||||||
|
"kids",
|
||||||
|
"reality",
|
||||||
|
"politics",
|
||||||
|
"soap",
|
||||||
|
"talk",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const externalid = () =>
|
||||||
|
jsonb()
|
||||||
|
.$type<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
dataId: string;
|
||||||
|
link: string | null;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>()
|
||||||
|
.notNull()
|
||||||
|
.default({});
|
||||||
|
|
||||||
|
export const shows = schema.table(
|
||||||
|
"shows",
|
||||||
|
{
|
||||||
|
pk: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
id: uuid().notNull().unique().defaultRandom(),
|
||||||
|
slug: varchar({ length: 255 }).notNull().unique(),
|
||||||
|
kind: showKind().notNull(),
|
||||||
|
genres: genres().array().notNull(),
|
||||||
|
rating: smallint(),
|
||||||
|
runtime: integer(),
|
||||||
|
status: showStatus().notNull(),
|
||||||
|
startAir: date(),
|
||||||
|
endAir: date(),
|
||||||
|
originalLanguage: language(),
|
||||||
|
|
||||||
|
externalId: externalid(),
|
||||||
|
|
||||||
|
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
check("rating_valid", sql`${t.rating} between 0 and 100`),
|
||||||
|
check("runtime_valid", sql`${t.runtime} >= 0`),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const showTranslations = schema.table(
|
||||||
|
"show_translations",
|
||||||
|
{
|
||||||
|
pk: integer()
|
||||||
|
.notNull()
|
||||||
|
.references(() => shows.pk, { onDelete: "cascade" }),
|
||||||
|
language: language().notNull(),
|
||||||
|
name: text().notNull(),
|
||||||
|
description: text(),
|
||||||
|
tagline: text(),
|
||||||
|
aliases: text().array().notNull(),
|
||||||
|
tags: text().array().notNull(),
|
||||||
|
poster: image(),
|
||||||
|
thumbnail: image(),
|
||||||
|
banner: image(),
|
||||||
|
logo: image(),
|
||||||
|
trailerUrl: text(),
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.pk, t.language] })],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const showsRelations = relations(shows, ({ many }) => ({
|
||||||
|
translations: many(showTranslations, { relationName: "showTranslations" }),
|
||||||
|
}));
|
||||||
|
export const showsTrRelations = relations(showTranslations, ({ one }) => ({
|
||||||
|
show: one(shows, {
|
||||||
|
relationName: "showTranslations",
|
||||||
|
fields: [showTranslations.pk],
|
||||||
|
references: [shows.pk],
|
||||||
|
}),
|
||||||
|
}));
|
79
api/src/db/schema/utils.ts
Normal file
79
api/src/db/schema/utils.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
is,
|
||||||
|
type ColumnsSelection,
|
||||||
|
type Subquery,
|
||||||
|
Table,
|
||||||
|
View,
|
||||||
|
ViewBaseConfig,
|
||||||
|
getTableColumns,
|
||||||
|
sql,
|
||||||
|
SQL,
|
||||||
|
} from "drizzle-orm";
|
||||||
|
import type { AnyMySqlSelect } from "drizzle-orm/mysql-core";
|
||||||
|
import {
|
||||||
|
type AnyPgSelect,
|
||||||
|
jsonb,
|
||||||
|
pgSchema,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import type { AnySQLiteSelect } from "drizzle-orm/sqlite-core";
|
||||||
|
import type { WithSubquery } from "drizzle-orm/subquery";
|
||||||
|
import { db } from "..";
|
||||||
|
import type { CasingCache } from "drizzle-orm/casing";
|
||||||
|
|
||||||
|
export const schema = pgSchema("kyoo");
|
||||||
|
|
||||||
|
export const language = () => varchar({ length: 255 });
|
||||||
|
|
||||||
|
export const image = () =>
|
||||||
|
jsonb().$type<{ id: string; source: string; blurhash: string }>();
|
||||||
|
|
||||||
|
// https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts#L58
|
||||||
|
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
|
||||||
|
|
||||||
|
// See https://github.com/drizzle-team/drizzle-orm/pull/1789
|
||||||
|
type Select = AnyPgSelect | AnyMySqlSelect | AnySQLiteSelect;
|
||||||
|
type AnySelect = Simplify<
|
||||||
|
Omit<Select, "where"> & Partial<Pick<Select, "where">>
|
||||||
|
>;
|
||||||
|
export function getColumns<
|
||||||
|
T extends
|
||||||
|
| Table
|
||||||
|
| View
|
||||||
|
| Subquery<string, ColumnsSelection>
|
||||||
|
| WithSubquery<string, ColumnsSelection>
|
||||||
|
| AnySelect,
|
||||||
|
>(
|
||||||
|
table: T,
|
||||||
|
): T extends Table
|
||||||
|
? T["_"]["columns"]
|
||||||
|
: T extends View | Subquery | WithSubquery | AnySelect
|
||||||
|
? T["_"]["selectedFields"]
|
||||||
|
: never {
|
||||||
|
return is(table, Table)
|
||||||
|
? (table as any)[(Table as any).Symbol.Columns]
|
||||||
|
: is(table, View)
|
||||||
|
? (table as any)[ViewBaseConfig].selectedFields
|
||||||
|
: table._.selectedFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/drizzle-team/drizzle-orm/issues/1728
|
||||||
|
export function conflictUpdateAllExcept<
|
||||||
|
T extends Table,
|
||||||
|
E extends (keyof T["_"]["columns"])[],
|
||||||
|
>(table: T, except: E) {
|
||||||
|
const columns = getTableColumns(table);
|
||||||
|
const updateColumns = Object.entries(columns).filter(
|
||||||
|
([col]) => !except.includes(col),
|
||||||
|
);
|
||||||
|
|
||||||
|
return updateColumns.reduce(
|
||||||
|
(acc, [colName, col]) => {
|
||||||
|
// @ts-expect-error: drizzle internal
|
||||||
|
const name = (db.dialect.casing as CasingCache).getColumnCasing(col);
|
||||||
|
acc[colName as keyof typeof acc] = sql.raw(`excluded."${name}"`);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Omit<Record<keyof T["_"]["columns"], SQL>, E[number]>,
|
||||||
|
);
|
||||||
|
}
|
48
api/src/db/schema/videos.ts
Normal file
48
api/src/db/schema/videos.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
check,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
primaryKey,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { schema } from "./utils";
|
||||||
|
import { entries } from "./entries";
|
||||||
|
|
||||||
|
export const videos = schema.table(
|
||||||
|
"videos",
|
||||||
|
{
|
||||||
|
pk: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
id: uuid().notNull().unique().defaultRandom(),
|
||||||
|
path: text().notNull().unique(),
|
||||||
|
rendering: text().notNull(),
|
||||||
|
part: integer(),
|
||||||
|
version: integer().notNull().default(1),
|
||||||
|
guess: jsonb().notNull().default({}),
|
||||||
|
|
||||||
|
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
check("part_pos", sql`${t.part} >= 0`),
|
||||||
|
check("version_pos", sql`${t.version} >= 0`),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const entryVideoJointure = schema.table(
|
||||||
|
"entry_video_jointure",
|
||||||
|
{
|
||||||
|
entry: integer()
|
||||||
|
.notNull()
|
||||||
|
.references(() => entries.pk, { onDelete: "cascade" }),
|
||||||
|
video: integer()
|
||||||
|
.notNull()
|
||||||
|
.references(() => videos.pk, { onDelete: "cascade" }),
|
||||||
|
slug: varchar({ length: 255 }).notNull().unique(),
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.entry, t.video] })],
|
||||||
|
);
|
82
api/src/index.ts
Normal file
82
api/src/index.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import jwt from "@elysiajs/jwt";
|
||||||
|
import { swagger } from "@elysiajs/swagger";
|
||||||
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||||
|
import { Elysia } from "elysia";
|
||||||
|
import { entries } from "./controllers/entries";
|
||||||
|
import { movies } from "./controllers/movies";
|
||||||
|
import { seasons } from "./controllers/seasons";
|
||||||
|
import { seed } from "./controllers/seed";
|
||||||
|
import { series } from "./controllers/series";
|
||||||
|
import { videos } from "./controllers/videos";
|
||||||
|
import { db } from "./db";
|
||||||
|
import { Image } from "./models/utils";
|
||||||
|
import { comment } from "./utils";
|
||||||
|
import { base } from "./base";
|
||||||
|
|
||||||
|
await migrate(db, { migrationsSchema: "kyoo", migrationsFolder: "./drizzle" });
|
||||||
|
|
||||||
|
let secret = process.env.JWT_SECRET;
|
||||||
|
if (!secret) {
|
||||||
|
const auth = process.env.AUTH_SERVER ?? "http://auth:4568";
|
||||||
|
try {
|
||||||
|
const ret = await fetch(`${auth}/info`);
|
||||||
|
const info = await ret.json();
|
||||||
|
secret = info.publicKey;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Can't access auth server at ${auth}:\n${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
console.error("Missing jwt secret or auth server. exiting");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(base)
|
||||||
|
.use(jwt({ secret }))
|
||||||
|
.use(
|
||||||
|
swagger({
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: "Kyoo",
|
||||||
|
description: comment`
|
||||||
|
Complete API documentation of Kyoo.
|
||||||
|
If you need a route not present here, please make an issue over https://github.com/zoriya/kyoo
|
||||||
|
`,
|
||||||
|
version: "5.0.0",
|
||||||
|
contact: { name: "github", url: "https://github.com/zoriya/kyoo" },
|
||||||
|
license: {
|
||||||
|
name: "GPL-3.0 license",
|
||||||
|
url: "https://github.com/zoriya/Kyoo/blob/master/LICENSE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: "https://kyoo.zoriya.dev/api",
|
||||||
|
description: "Kyoo's demo server",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tags: [
|
||||||
|
{ name: "movies", description: "Routes about movies" },
|
||||||
|
{
|
||||||
|
name: "videos",
|
||||||
|
description: comment`
|
||||||
|
Used by the scanner internally to list & create videos.
|
||||||
|
Can be used for administration or third party apps.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.model({ image: Image })
|
||||||
|
.use(movies)
|
||||||
|
.use(series)
|
||||||
|
.use(entries)
|
||||||
|
.use(seasons)
|
||||||
|
.use(videos)
|
||||||
|
.use(seed)
|
||||||
|
.listen(3000);
|
||||||
|
|
||||||
|
console.log(`Api running at ${app.server?.hostname}:${app.server?.port}`);
|
31
api/src/models/entry/base-entry.ts
Normal file
31
api/src/models/entry/base-entry.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { Image } from "../utils/image";
|
||||||
|
|
||||||
|
export const BaseEntry = t.Object({
|
||||||
|
airDate: t.Nullable(t.String({ format: "data" })),
|
||||||
|
runtime: t.Nullable(
|
||||||
|
t.Number({ minimum: 0, description: "Runtime of the episode in minutes" }),
|
||||||
|
),
|
||||||
|
thumbnail: t.Nullable(Image),
|
||||||
|
|
||||||
|
createdAt: t.String({ format: "date-time" }),
|
||||||
|
nextRefresh: t.String({ format: "date-time" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const EntryTranslation = t.Object({
|
||||||
|
name: t.Nullable(t.String()),
|
||||||
|
description: t.Nullable(t.String()),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// export const SeedEntry = t.Intersect([
|
||||||
|
// Entry,
|
||||||
|
// t.Object({ videos: t.Optional(t.Array(Video)) }),
|
||||||
|
// ]);
|
||||||
|
// export type SeedEntry = typeof SeedEntry.static;
|
||||||
|
//
|
||||||
|
// export const SeedExtra = t.Intersect([
|
||||||
|
// Extra,
|
||||||
|
// t.Object({ video: t.Optional(Video) }),
|
||||||
|
// ]);
|
||||||
|
// export type SeedExtra = typeof SeedExtra.static;
|
18
api/src/models/entry/episode.ts
Normal file
18
api/src/models/entry/episode.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
import { EpisodeId } from "../utils/external-id";
|
||||||
|
import { Resource } from "../utils/resource";
|
||||||
|
|
||||||
|
export const BaseEpisode = t.Intersect([
|
||||||
|
BaseEntry,
|
||||||
|
t.Object({
|
||||||
|
kind: t.Literal("episode"),
|
||||||
|
order: t.Number({ minimum: 1, description: "Absolute playback order." }),
|
||||||
|
seasonNumber: t.Number(),
|
||||||
|
episodeNumber: t.Number(),
|
||||||
|
externalId: EpisodeId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const Episode = t.Intersect([Resource, BaseEpisode, EntryTranslation]);
|
||||||
|
export type Episode = typeof Episode.static;
|
37
api/src/models/entry/extra.ts
Normal file
37
api/src/models/entry/extra.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
import { EpisodeId } from "../utils/external-id";
|
||||||
|
import { comment } from "../../utils";
|
||||||
|
import { Resource } from "../utils/resource";
|
||||||
|
|
||||||
|
export const ExtraType = t.UnionEnum([
|
||||||
|
"other",
|
||||||
|
"trailers",
|
||||||
|
"interview",
|
||||||
|
"behind-the-scenes",
|
||||||
|
"deleted-scenes",
|
||||||
|
"bloopers",
|
||||||
|
]);
|
||||||
|
export type ExtraType = typeof ExtraType.static;
|
||||||
|
|
||||||
|
export const BaseExtra = t.Intersect(
|
||||||
|
[
|
||||||
|
BaseEntry,
|
||||||
|
t.Object({
|
||||||
|
kind: ExtraType,
|
||||||
|
// not sure about this id type
|
||||||
|
externalId: EpisodeId,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
description: comment`
|
||||||
|
An extra can be a beyond-the-scene, short-episodes or anything that is in a different format & not required
|
||||||
|
in the main story plot.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Extra = t.Intersect([Resource, BaseExtra, EntryTranslation]);
|
||||||
|
export type Extra = typeof Extra.static;
|
||||||
|
|
||||||
|
|
11
api/src/models/entry/index.ts
Normal file
11
api/src/models/entry/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { Episode, MovieEntry, Special } from "../entry";
|
||||||
|
|
||||||
|
export const Entry = t.Union([Episode, MovieEntry, Special]);
|
||||||
|
export type Entry = typeof Entry.static;
|
||||||
|
|
||||||
|
export * from "./episode";
|
||||||
|
export * from "./movie-entry";
|
||||||
|
export * from "./special";
|
||||||
|
export * from "./extra";
|
||||||
|
export * from "./unknown-entry";
|
41
api/src/models/entry/movie-entry.ts
Normal file
41
api/src/models/entry/movie-entry.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "../../utils";
|
||||||
|
import { ExternalId } from "../utils/external-id";
|
||||||
|
import { Image } from "../utils/image";
|
||||||
|
import { Resource } from "../utils/resource";
|
||||||
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
|
export const BaseMovieEntry = t.Intersect(
|
||||||
|
[
|
||||||
|
t.Omit(BaseEntry, ["thumbnail"]),
|
||||||
|
t.Object({
|
||||||
|
kind: t.Literal("movie"),
|
||||||
|
order: t.Number({
|
||||||
|
minimum: 1,
|
||||||
|
description: "Absolute playback order. Can be mixed with episodes.",
|
||||||
|
}),
|
||||||
|
externalId: ExternalId,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
description: comment`
|
||||||
|
If a movie is part of a serie (watching the movie require context from the serie &
|
||||||
|
the next episode of the serie require you to have seen the movie to understand it.)
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MovieEntryTranslation = t.Intersect([
|
||||||
|
EntryTranslation,
|
||||||
|
t.Object({
|
||||||
|
tagline: t.Nullable(t.String()),
|
||||||
|
thumbnail: t.Nullable(Image),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const MovieEntry = t.Intersect([
|
||||||
|
Resource,
|
||||||
|
BaseMovieEntry,
|
||||||
|
MovieEntryTranslation,
|
||||||
|
]);
|
||||||
|
export type MovieEntry = typeof MovieEntry.static;
|
29
api/src/models/entry/special.ts
Normal file
29
api/src/models/entry/special.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "../../utils";
|
||||||
|
import { EpisodeId } from "../utils/external-id";
|
||||||
|
import { Resource } from "../utils/resource";
|
||||||
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
|
export const BaseSpecial = t.Intersect(
|
||||||
|
[
|
||||||
|
BaseEntry,
|
||||||
|
t.Object({
|
||||||
|
kind: t.Literal("special"),
|
||||||
|
order: t.Number({
|
||||||
|
minimum: 1,
|
||||||
|
description: "Absolute playback order. Can be mixed with episodes.",
|
||||||
|
}),
|
||||||
|
number: t.Number({ minimum: 1 }),
|
||||||
|
externalId: EpisodeId,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
description: comment`
|
||||||
|
A special is either an OAV episode (side story & co) or an important episode that was released standalone
|
||||||
|
(outside of a season.)
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Special = t.Intersect([Resource, BaseSpecial, EntryTranslation]);
|
||||||
|
export type Special = typeof Special.static;
|
30
api/src/models/entry/unknown-entry.ts
Normal file
30
api/src/models/entry/unknown-entry.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "../../utils";
|
||||||
|
import { Resource } from "../utils/resource";
|
||||||
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
|
export const BaseUnknownEntry = t.Intersect(
|
||||||
|
[
|
||||||
|
t.Omit(BaseEntry, ["airDate"]),
|
||||||
|
t.Object({
|
||||||
|
kind: t.Literal("unknown"),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
description: comment`
|
||||||
|
A video not releated to any series or movie. This can be due to a matching error but it can be a youtube
|
||||||
|
video or any other video content.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UnknownEntryTranslation = t.Omit(EntryTranslation, [
|
||||||
|
"description",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const UnknownEntry = t.Intersect([
|
||||||
|
Resource,
|
||||||
|
BaseUnknownEntry,
|
||||||
|
UnknownEntryTranslation,
|
||||||
|
]);
|
||||||
|
export type UnknownEntry = typeof UnknownEntry.static;
|
14
api/src/models/error.ts
Normal file
14
api/src/models/error.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
|
export const KError = t.Object({
|
||||||
|
status: t.Integer(),
|
||||||
|
message: t.String(),
|
||||||
|
details: t.Optional(t.Any()),
|
||||||
|
});
|
||||||
|
export type KError = typeof KError.static;
|
||||||
|
|
||||||
|
export class KErrorT extends Error {
|
||||||
|
constructor(message: string, details?: any) {
|
||||||
|
super(JSON.stringify({ code: "KError", status: 422, message, details }));
|
||||||
|
}
|
||||||
|
}
|
72
api/src/models/examples/bubble.ts
Normal file
72
api/src/models/examples/bubble.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import type { SeedMovie } from "../movie";
|
||||||
|
import type { Video } from "../video";
|
||||||
|
|
||||||
|
export const bubbleVideo: Video = {
|
||||||
|
id: "3cd436ee-01ff-4f45-ba98-62aabeb22f25",
|
||||||
|
slug: "bubble",
|
||||||
|
path: "/video/Bubble/Bubble (2022).mkv",
|
||||||
|
rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-11-23T15:01:24.968Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bubble: SeedMovie = {
|
||||||
|
slug: "bubble",
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
name: "Bubble",
|
||||||
|
tagline: "Is she a calamity or a blessing?",
|
||||||
|
description:
|
||||||
|
"In an abandoned Tokyo overrun by bubbles and gravitational abnormalities, one gifted young man has a fateful meeting with a mysterious girl.",
|
||||||
|
aliases: ["Baburu", "バブル:2022", "Bubble"],
|
||||||
|
tags: ["adolescence", "disaster", "battle", "gravity", "anime"],
|
||||||
|
poster:
|
||||||
|
"https://image.tmdb.org/t/p/original/65dad96VE8FJPEdrAkhdsuWMWH9.jpg",
|
||||||
|
thumbnail:
|
||||||
|
"https://image.tmdb.org/t/p/original/a8Q2g0g7XzAF6gcB8qgn37ccb9Y.jpg",
|
||||||
|
banner: null,
|
||||||
|
logo: "https://image.tmdb.org/t/p/original/ihIs7fayAmZieMlMQbs6TWM77uf.png",
|
||||||
|
trailerUrl: "https://www.youtube.com/watch?v=vs7zsyIZkMM",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
genres: ["animation", "adventure", "science-fiction", "fantasy"],
|
||||||
|
rating: 74,
|
||||||
|
status: "finished",
|
||||||
|
runtime: 101,
|
||||||
|
airDate: "2022-02-14",
|
||||||
|
originalLanguage: "ja",
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
dataId: "912598",
|
||||||
|
link: "https://www.themoviedb.org/movie/912598",
|
||||||
|
},
|
||||||
|
imdb: {
|
||||||
|
dataId: "tt16360006",
|
||||||
|
link: "https://www.imdb.com/title/tt16360006",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
videos: [bubbleVideo.id],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bubbleImages = {
|
||||||
|
poster: {
|
||||||
|
id: "befdc7dd-2a67-0704-92af-90d49eee0315",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/65dad96VE8FJPEdrAkhdsuWMWH9.jpg",
|
||||||
|
blurhash: "LFC@2F;K$%xZ5?W.MwNF0iD~MxR:",
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
id: "b29908f3-a64d-ae98-923b-18bf7995ab04",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/a8Q2g0g7XzAF6gcB8qgn37ccb9Y.jpg",
|
||||||
|
blurhash: "LpH3afE1XAveyGS7t6V[R4xZn+S6",
|
||||||
|
},
|
||||||
|
banner: null,
|
||||||
|
logo: {
|
||||||
|
id: "3357fad0-de40-4ca5-15e6-eb065d35be86",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/ihIs7fayAmZieMlMQbs6TWM77uf.png",
|
||||||
|
blurhash: "LMDc5#MwE0,sTKE0R*S~4mxunhb_",
|
||||||
|
},
|
||||||
|
};
|
72
api/src/models/examples/dune-1984.ts
Normal file
72
api/src/models/examples/dune-1984.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import type { SeedMovie } from "../movie";
|
||||||
|
import type { Video } from "../video";
|
||||||
|
|
||||||
|
export const dune1984Video: Video = {
|
||||||
|
id: "d1a62b87-9cfd-4f9c-9ad7-21f9b7fa6290",
|
||||||
|
slug: "dune-1984",
|
||||||
|
path: "/video/Dune_1984/Dune (1984).mkv",
|
||||||
|
rendering: "ea3a0f8f2f2c5b61a07f61e4e8d9f8e01b2b92bcbb6f5ed1151e1f61619c2c0f",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-12-02T11:45:12.968Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dune1984: SeedMovie = {
|
||||||
|
slug: "dune-1984",
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
name: "Dune",
|
||||||
|
tagline: "A journey to the stars begins with a single step.",
|
||||||
|
description:
|
||||||
|
"On the planet Arrakis, the young Paul Atreides and his family are thrust into a world of political intrigue and warfare over control of the spice melange, the most valuable substance in the universe.",
|
||||||
|
aliases: ["Dune 1984", "Dune: David Lynch's Vision", "Dune: The Movie"],
|
||||||
|
tags: ["sci-fi", "adventure", "drama", "cult-classic", "epic"],
|
||||||
|
poster:
|
||||||
|
"https://image.tmdb.org/t/p/original/eVnVrIWkT8esL3XsTc4BjhDhQKq.jpg",
|
||||||
|
thumbnail:
|
||||||
|
"https://image.tmdb.org/t/p/original/pCHV6BntWLO2H6wQOj4LwzAWqpa.jpg",
|
||||||
|
banner: null,
|
||||||
|
logo: "https://image.tmdb.org/t/p/original/olbKnk2VvFcM2STl0dJAf6kfydo.png",
|
||||||
|
trailerUrl: "https://www.youtube.com/watch?v=vczYTLQ6oiE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
genres: ["adventure", "drama", "science-fiction"],
|
||||||
|
rating: 60,
|
||||||
|
status: "finished",
|
||||||
|
runtime: 137,
|
||||||
|
airDate: "1984-12-14",
|
||||||
|
originalLanguage: "en",
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
dataId: "9495",
|
||||||
|
link: "https://www.themoviedb.org/movie/9495",
|
||||||
|
},
|
||||||
|
imdb: {
|
||||||
|
dataId: "tt0087182",
|
||||||
|
link: "https://www.imdb.com/title/tt0087182",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
videos: [dune1984Video.id],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dune1984Images = {
|
||||||
|
poster: {
|
||||||
|
id: "a5e1c5e4-4176-42f0-a279-8ab6f1ae2d30",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/eVnVrIWkT8esL3XsTc4BjhDhQKq.jpg",
|
||||||
|
blurhash: "L32^9tc~%8~U%OItfNGq9FoLV@X9",
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
id: "fe44141b-58bc-42b7-a5c5-e10b801e99ae",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/pCHV6BntWLO2H6wQOj4LwzAWqpa.jpg",
|
||||||
|
blurhash: "L56~XM~q9ZZX4wbD9Wa|ECxvS~V@",
|
||||||
|
},
|
||||||
|
banner: null,
|
||||||
|
logo: {
|
||||||
|
id: "515d7d72-b4f0-4a7d-a27a-eac3495ea8b3",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/olbKnk2VvFcM2STl0dJAf6kfydo.png",
|
||||||
|
blurhash: "LJ4XXK*]JFMzM]V?~Xz$sV?tMdm+",
|
||||||
|
},
|
||||||
|
};
|
72
api/src/models/examples/dune-2021.ts
Normal file
72
api/src/models/examples/dune-2021.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import type { SeedMovie } from "../movie";
|
||||||
|
import type { Video } from "../video";
|
||||||
|
|
||||||
|
export const duneVideo: Video = {
|
||||||
|
id: "c9a0d02e-6b8e-4ac1-b431-45b022ec0708",
|
||||||
|
slug: "dune",
|
||||||
|
path: "/video/Dune/Dune (2021).mkv",
|
||||||
|
rendering: "f1953a4fb58247efb6c15b76468b6a9d13b4155b02094863b1a4f0c3fbb6db58",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-12-02T10:10:24.968Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dune: SeedMovie = {
|
||||||
|
slug: "dune",
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
name: "Dune",
|
||||||
|
tagline: "A mythic and emotionally charged hero's journey.",
|
||||||
|
description:
|
||||||
|
"On the desert planet Arrakis, a young nobleman becomes embroiled in a complex struggle for control of the planet's valuable resource, the spice melange.",
|
||||||
|
aliases: ["Dune: Part One", "Dune 2021"],
|
||||||
|
tags: ["sci-fi", "adventure", "drama", "action", "epic"],
|
||||||
|
poster:
|
||||||
|
"https://image.tmdb.org/t/p/original/wD57HqZ6fXwwDdfQLo4hXLRwGV1.jpg",
|
||||||
|
thumbnail:
|
||||||
|
"https://image.tmdb.org/t/p/original/k2ocXnNkmvE6rJomRkExIStFq3v.jpg",
|
||||||
|
banner: null,
|
||||||
|
logo: "https://image.tmdb.org/t/p/original/5nDsd3u1c6kDphbtIqkHseLg7HL.png",
|
||||||
|
trailerUrl: "https://www.youtube.com/watch?v=n9xhJrPXop4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
genres: ["adventure", "drama", "science-fiction", "action"],
|
||||||
|
rating: 83,
|
||||||
|
status: "finished",
|
||||||
|
runtime: 155,
|
||||||
|
airDate: "2021-10-22",
|
||||||
|
originalLanguage: "en",
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
dataId: "496243",
|
||||||
|
link: "https://www.themoviedb.org/movie/496243",
|
||||||
|
},
|
||||||
|
imdb: {
|
||||||
|
dataId: "tt1160419",
|
||||||
|
link: "https://www.imdb.com/title/tt1160419",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
videos: [duneVideo.id],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const duneImages = {
|
||||||
|
poster: {
|
||||||
|
id: "ea0426d1-4d16-4be9-9e6f-08e5fdf8f209",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/wD57HqZ6fXwwDdfQLo4hXLRwGV1.jpg",
|
||||||
|
blurhash: "L3D8AK$A5l=j~Bt7_4Mw-;WBt4Gf",
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
id: "1b629b7f-3b44-45b9-9432-cb5505045899",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/k2ocXnNkmvE6rJomRkExIStFq3v.jpg",
|
||||||
|
blurhash: "L6l5}7$S0nt7p~2R.9W9tQ%NflWC",
|
||||||
|
},
|
||||||
|
banner: null,
|
||||||
|
logo: {
|
||||||
|
id: "c02ec0d2-d04e-4f51-8d4e-4cdd9ca75a7e",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/5nDsd3u1c6kDphbtIqkHseLg7HL.png",
|
||||||
|
blurhash: "LLOQ0t-7e,X6jY?qBtt6c8A4gYof",
|
||||||
|
},
|
||||||
|
};
|
34
api/src/models/examples/index.ts
Normal file
34
api/src/models/examples/index.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { TSchema } from "elysia";
|
||||||
|
import { KindGuard } from "@sinclair/typebox";
|
||||||
|
|
||||||
|
export const registerExamples = <T extends TSchema>(
|
||||||
|
schema: T,
|
||||||
|
...examples: (Partial<T["static"] >| undefined)[]
|
||||||
|
) => {
|
||||||
|
if (KindGuard.IsUnion(schema)) {
|
||||||
|
for (const union of schema.anyOf) {
|
||||||
|
registerExamples(union, ...examples);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (KindGuard.IsIntersect(schema)) {
|
||||||
|
for (const intersec of schema.allOf) {
|
||||||
|
registerExamples(intersec, ...examples);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const example of examples) {
|
||||||
|
if (!example) continue;
|
||||||
|
for (const [key, val] of Object.entries(example)) {
|
||||||
|
const prop = schema.properties[
|
||||||
|
key as keyof typeof schema.properties
|
||||||
|
] as TSchema;
|
||||||
|
if (!prop) continue;
|
||||||
|
prop.examples ??= [];
|
||||||
|
prop.examples.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from "./bubble";
|
||||||
|
export * from "./made-in-abyss";
|
340
api/src/models/examples/made-in-abyss.ts
Normal file
340
api/src/models/examples/made-in-abyss.ts
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
import type { SeedSerie } from "../seed";
|
||||||
|
|
||||||
|
export const madeInAbyss: SeedSerie = {
|
||||||
|
id: "04bcf2ac-3c09-42f6-8357-b003798f9562",
|
||||||
|
slug: "made-in-abyss",
|
||||||
|
name: "Made in Abyss",
|
||||||
|
tagline: "How far would you go… for the ones you love?",
|
||||||
|
aliases: [
|
||||||
|
"Made in Abyss: The Golden City of the Scorching Sun",
|
||||||
|
"Meidoinabisu",
|
||||||
|
"Meidoinabisu: Retsujitsu no ôgonkyô",
|
||||||
|
],
|
||||||
|
description:
|
||||||
|
"Located in the center of a remote island, the Abyss is the last unexplored region, a huge and treacherous fathomless hole inhabited by strange creatures where only the bravest adventurers descend in search of ancient relics. In the upper levels of the Abyss, Riko, a girl who dreams of becoming an explorer, stumbles upon a mysterious little boy.",
|
||||||
|
tags: [
|
||||||
|
"android",
|
||||||
|
"amnesia",
|
||||||
|
"post-apocalyptic future",
|
||||||
|
"exploration",
|
||||||
|
"friendship",
|
||||||
|
"mecha",
|
||||||
|
"survival",
|
||||||
|
"curse",
|
||||||
|
"tragedy",
|
||||||
|
"orphan",
|
||||||
|
"based on manga",
|
||||||
|
"robot",
|
||||||
|
"dark fantasy",
|
||||||
|
"seinen",
|
||||||
|
"anime",
|
||||||
|
"drastic change of life",
|
||||||
|
"fantasy",
|
||||||
|
"adventure",
|
||||||
|
],
|
||||||
|
genres: [
|
||||||
|
"animation",
|
||||||
|
"drama",
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"science-fiction",
|
||||||
|
"fantasy",
|
||||||
|
],
|
||||||
|
status: "finished",
|
||||||
|
rating: 84,
|
||||||
|
runtime: 24,
|
||||||
|
originalLanguage: "ja",
|
||||||
|
startAir: "2017-07-07",
|
||||||
|
endAir: "2022-09-28",
|
||||||
|
poster: {
|
||||||
|
id: "8205a20e-d91f-804c-3a84-4e4dc6202d66",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/4Bh9qzB1Kau4RDaVQXVFdoJ0HcE.jpg",
|
||||||
|
blurhash: "LZGlS3XTD%jE~Wf,SeV@%2o|WERj",
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
id: "819d816c-88f6-9f3a-b5e7-ce3daaffbac4",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/Df9XrvZFIeQfLKfu8evRmzvRsd.jpg",
|
||||||
|
blurhash: "LmJtk{kq~q%2bbWCxaV@.8RixuNG",
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
id: "23cb7b06-8406-2288-8e40-08bfc16180b5",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/7hY3Q4GhkiYPBfn4UoVg0AO4Zgk.png",
|
||||||
|
blurhash: "LKGaa%M{0zbI#7$%bbofGGw^wcw{",
|
||||||
|
},
|
||||||
|
banner: null,
|
||||||
|
trailerUrl: "https://www.youtube.com/watch?v=ePOyy6Wlk4s",
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
dataId: "72636",
|
||||||
|
link: "https://www.themoviedb.org/tv/72636",
|
||||||
|
},
|
||||||
|
imdb: { dataId: "tt7222086", link: "https://www.imdb.com/title/tt7222086" },
|
||||||
|
tvdb: { dataId: "326109", link: null },
|
||||||
|
},
|
||||||
|
createdAt: "2023-11-29T11:12:11.949503Z",
|
||||||
|
nextRefresh: "2025-01-07T11:42:50.948248Z",
|
||||||
|
seasons: [
|
||||||
|
{
|
||||||
|
id: "490aa312-53b9-43c2-845d-7cbf32642c98",
|
||||||
|
slug: "made-in-abyss-s1",
|
||||||
|
seasonNumber: 1,
|
||||||
|
name: "Season 1",
|
||||||
|
description:
|
||||||
|
"Within the depths of the Abyss, a girl named Riko stumbles upon a robot who looks like a young boy. Riko and her new friend descend into uncharted territory to unlock its mysteries, but what lies in wait for them in the darkness?",
|
||||||
|
startAir: "2017-07-07",
|
||||||
|
endAir: "2017-09-29",
|
||||||
|
poster: {
|
||||||
|
id: "1c121a2b-d3a2-4ce8-e22a-79b13dde3f7d",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/uVK3H8CgtrVgySFpdImvNXkN7RK.jpg",
|
||||||
|
blurhash: "LYG9BNkrD%V?~WS5S1WA%LbubHV[",
|
||||||
|
},
|
||||||
|
thumbnail: null,
|
||||||
|
banner: null,
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 1,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: "2023-11-29T11:12:13.008151Z",
|
||||||
|
nextRefresh: "2025-01-07T11:37:50.151836Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "135af9ae-a8eb-4110-a4e4-05eee49e2d76",
|
||||||
|
slug: "made-in-abyss-s2",
|
||||||
|
seasonNumber: 2,
|
||||||
|
name: "The Golden City of the Scorching Sun",
|
||||||
|
description:
|
||||||
|
"Set directly after the events of Made in Abyss: Dawn of the Deep Soul, the fifth installment of Made in Abyss covers the adventure of Reg, Riko and Nanachi in the Sixth Layer, The Capital of the Unreturned.",
|
||||||
|
startAir: "2022-07-06",
|
||||||
|
endAir: "2022-09-28",
|
||||||
|
poster: {
|
||||||
|
id: "a03c57d7-4032-7d97-083a-9a6e51d5f1e7",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/clC2erfUqIezhET67Gz9fcKD1L2.jpg",
|
||||||
|
blurhash: "LpNTRGx]s9oz~WbJRPoft7RjV@a|",
|
||||||
|
},
|
||||||
|
thumbnail: null,
|
||||||
|
banner: null,
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 2,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: "2023-11-29T11:12:13.630306Z",
|
||||||
|
nextRefresh: "2025-01-07T11:09:19.552971Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
kind: "episode",
|
||||||
|
id: "ab912364-61c8-4752-ac93-5802212467d8",
|
||||||
|
slug: "made-in-abyss-s1e13",
|
||||||
|
order: 13,
|
||||||
|
seasonNumber: 1,
|
||||||
|
episodeNumber: 13,
|
||||||
|
name: "The Challengers",
|
||||||
|
description:
|
||||||
|
"Nanachi and Mitty's past is revealed. How did they become what they are and who is responsible for it? Meanwhile, Riko is on the mend after her injuries.",
|
||||||
|
runtime: 47,
|
||||||
|
airDate: "2017-09-29",
|
||||||
|
thumbnail: {
|
||||||
|
id: "c2bfd626-bfdb-dee8-caa6-b6a7e7cb74ad",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/j9t1quh24suXxBetV7Q77YngID6.jpg",
|
||||||
|
blurhash: "L370#nD*^jEN}r$$$%J8i_-URkNc",
|
||||||
|
},
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 1,
|
||||||
|
episode: 13,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/1/episode/13",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: "2024-10-06T20:09:09.28103Z",
|
||||||
|
nextRefresh: "2024-12-06T20:08:42.366583Z",
|
||||||
|
videos: [
|
||||||
|
{
|
||||||
|
id: "0905bddd-8b93-403c-9b9c-db472e55d6cc",
|
||||||
|
slug: "made-in-abyss-s1e13",
|
||||||
|
path: "/video/Made in Abyss/Made in Abyss S01E13.mkv",
|
||||||
|
rendering:
|
||||||
|
"e27f226fe5e8d87cd396d0c3d24e1b1135aa563fcfca081bf68c6a71b44de107",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-10-06T20:09:09.28103Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "special",
|
||||||
|
id: "1a83288a-3089-447f-9710-94297d614c51",
|
||||||
|
slug: "made-in-abyss-ova3",
|
||||||
|
// beween s1e13 & movie (which has 13.5 for the `order field`)
|
||||||
|
order: 13.25,
|
||||||
|
number: 3,
|
||||||
|
name: "Maruruk's Everday 3 - Cleaning",
|
||||||
|
description:
|
||||||
|
"Short played before Made in Abyss Movie 3: Dawn of the Deep Soul in Japan's theatrical screenings before the main movie from 2020-01-17 to 2020-01-23.",
|
||||||
|
runtime: 3,
|
||||||
|
airDate: "2020-01-31",
|
||||||
|
thumbnail: {
|
||||||
|
id: "f4ac4b0a-c857-ea95-4042-601314a26e71",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
|
||||||
|
blurhash: "LAD,Pg%dc}tPDQfk.7kBo|ayR7WC",
|
||||||
|
},
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 0,
|
||||||
|
episode: 3,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/0/episode/3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||||
|
nextRefresh: "2024-12-06T20:08:29.463394Z",
|
||||||
|
videos: [
|
||||||
|
{
|
||||||
|
id: "9153f7dc-b635-4a04-a2db-9c08ea205ec3",
|
||||||
|
slug: "made-in-abyss-ova3",
|
||||||
|
path: "/video/Made in Abyss/Made in Abyss S00E03.mkv",
|
||||||
|
rendering:
|
||||||
|
"0391acf2268983de705f65381d252f1b0cd3c3563209303dc50cf71ab400ebf4",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "movie",
|
||||||
|
id: "59312db0-df8c-446e-be26-2b2107d0cbde",
|
||||||
|
slug: "made-in-abyss-dawn-of-the-deep-soul",
|
||||||
|
order: 13.5,
|
||||||
|
name: "Made in Abyss: Dawn of the Deep Soul",
|
||||||
|
tagline: "Defy the darkness",
|
||||||
|
description:
|
||||||
|
"A continuation of the epic adventure of plucky Riko and Reg who are joined by their new friend Nanachi. Together they descend into the Abyss' treacherous fifth layer, the Sea of Corpses, and encounter the mysterious Bondrewd, a legendary White Whistle whose shadow looms over Nanachi's troubled past. Bondrewd is ingratiatingly hospitable, but the brave adventurers know things are not always as they seem in the enigmatic Abyss.",
|
||||||
|
runtime: 105,
|
||||||
|
airDate: "2020-01-17",
|
||||||
|
poster: {
|
||||||
|
id: "f4ac4b0a-c857-ea95-4042-601314a26e71",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
|
||||||
|
blurhash: "LAD,Pg%dc}tPDQfk.7kBo|ayR7WC",
|
||||||
|
},
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
dataId: "72636",
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/0/episode/3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||||
|
nextRefresh: "2024-12-06T20:08:29.463394Z",
|
||||||
|
videos: [
|
||||||
|
{
|
||||||
|
id: "d3cedfc5-23f4-4aab-b4d3-98bef2954442",
|
||||||
|
slug: "made-in-abyss-dawn-of-the-deep-soul",
|
||||||
|
path: "/video/Made in Abyss/Made in Abyss Dawn of the Deep Soul.mkv",
|
||||||
|
rendering:
|
||||||
|
"a59ba5d88a4935d900db312422eec6f16827ce2572cc8c0eb6c8fffc5e235d6d",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "episode",
|
||||||
|
id: "bd155be3-39d0-4253-bb29-a60bedb62943",
|
||||||
|
slug: "made-in-abyss-s2e1",
|
||||||
|
order: 14,
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 1,
|
||||||
|
name: "The Compass Pointed to the Darkness",
|
||||||
|
description:
|
||||||
|
"An old man speaks of a golden city that lies within a devouring abyss somewhere in uncharted waters. One explorer may be the key to finding both.",
|
||||||
|
runtime: 23,
|
||||||
|
airDate: "2022-07-06",
|
||||||
|
thumbnail: {
|
||||||
|
id: "072da617-f349-4a68-eb27-d097624b373c",
|
||||||
|
source:
|
||||||
|
"https://image.tmdb.org/t/p/original/Tgu6E3aMf7sFHFbEIMEjetnpMi.jpg",
|
||||||
|
blurhash: "LOI#x]yE01xtE2D*kWt7NGjENGM|",
|
||||||
|
},
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 2,
|
||||||
|
episode: 1,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/2/episode/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||||
|
nextRefresh: "2024-12-06T20:08:22.854073Z",
|
||||||
|
videos: [
|
||||||
|
{
|
||||||
|
id: "3cbcc337-f1da-486a-93bd-c705a58545eb",
|
||||||
|
slug: "made-in-abyss-s2e1-p1",
|
||||||
|
path: "/video/Made in Abyss/Made In Abyss S02E01 Part 1.mkv",
|
||||||
|
rendering:
|
||||||
|
"6239d558696fd1cbcd70a67346e748382fe141bbe7ea01a5d702cdcc02aa996f",
|
||||||
|
part: 1,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "67b37a00-7459-4287-9bbf-e058675850b5",
|
||||||
|
slug: "made-in-abyss-s2e1-p2",
|
||||||
|
path: "/video/Made in Abyss/Made In Abyss S02E01 Part 2.mkv",
|
||||||
|
rendering:
|
||||||
|
"6239d558696fd1cbcd70a67346e748382fe141bbe7ea01a5d702cdcc02aa996f",
|
||||||
|
part: 2,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
kind: "behind-the-scenes",
|
||||||
|
id: "a9b27fcc-9423-44ad-b875-d35a7a25b613",
|
||||||
|
slug: "made-in-abyss-the-making-of-01",
|
||||||
|
name: "The Making of MADE IN ABYSS 01",
|
||||||
|
description: null,
|
||||||
|
runtime: 17,
|
||||||
|
airDate: "2017-10-25",
|
||||||
|
thumbnail: null,
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 0,
|
||||||
|
episode: 13,
|
||||||
|
link: "https://thetvdb.com/series/made-in-abyss/episodes/8835068",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||||
|
nextRefresh: "2024-12-06T20:08:22.854073Z",
|
||||||
|
video: {
|
||||||
|
id: "ee3f58eb-0f72-423e-b247-0695cfabfa88",
|
||||||
|
slug: "made-in-abyss-s2e1-p2",
|
||||||
|
path: "/video/Made in Abyss/Made In Abyss S02E01 Part 2.mkv",
|
||||||
|
rendering:
|
||||||
|
"6239d558696fd1cbcd70a67346e748382fe141bbe7ea01a5d702cdcc02aa996f",
|
||||||
|
part: 2,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
78
api/src/models/movie.ts
Normal file
78
api/src/models/movie.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { ExternalId, Genre, Image, Language, SeedImage } from "./utils";
|
||||||
|
import { bubble, registerExamples } from "./examples";
|
||||||
|
import { bubbleImages } from "./examples/bubble";
|
||||||
|
|
||||||
|
export const MovieStatus = t.UnionEnum(["unknown", "finished", "planned"]);
|
||||||
|
export type MovieStatus = typeof MovieStatus.static;
|
||||||
|
|
||||||
|
const BaseMovie = t.Object({
|
||||||
|
id: t.String({ format: "uuid" }),
|
||||||
|
slug: t.String({ format: "slug" }),
|
||||||
|
genres: t.Array(Genre),
|
||||||
|
rating: t.Nullable(t.Number({ minimum: 0, maximum: 100 })),
|
||||||
|
status: MovieStatus,
|
||||||
|
runtime: t.Nullable(
|
||||||
|
t.Number({ minimum: 0, description: "Runtime of the movie in minutes." }),
|
||||||
|
),
|
||||||
|
|
||||||
|
airDate: t.Nullable(t.String({ format: "date" })),
|
||||||
|
originalLanguage: t.Nullable(
|
||||||
|
Language({
|
||||||
|
description: "The language code this movie was made in.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
createdAt: t.String({ format: "date-time" }),
|
||||||
|
nextRefresh: t.String({ format: "date-time" }),
|
||||||
|
|
||||||
|
externalId: ExternalId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MovieTranslation = t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
description: t.Nullable(t.String()),
|
||||||
|
tagline: t.Nullable(t.String()),
|
||||||
|
aliases: t.Array(t.String()),
|
||||||
|
tags: t.Array(t.String()),
|
||||||
|
|
||||||
|
poster: t.Nullable(Image),
|
||||||
|
thumbnail: t.Nullable(Image),
|
||||||
|
banner: t.Nullable(Image),
|
||||||
|
logo: t.Nullable(Image),
|
||||||
|
trailerUrl: t.Nullable(t.String()),
|
||||||
|
});
|
||||||
|
export type MovieTranslation = typeof MovieTranslation.static;
|
||||||
|
|
||||||
|
export const Movie = t.Intersect([BaseMovie, MovieTranslation]);
|
||||||
|
export type Movie = typeof Movie.static;
|
||||||
|
|
||||||
|
export const SeedMovie = t.Intersect([
|
||||||
|
t.Omit(BaseMovie, ["id", "createdAt", "nextRefresh"]),
|
||||||
|
t.Object({
|
||||||
|
translations: t.Record(
|
||||||
|
Language(),
|
||||||
|
t.Intersect([
|
||||||
|
t.Omit(MovieTranslation, ["poster", "thumbnail", "banner", "logo"]),
|
||||||
|
t.Object({
|
||||||
|
poster: t.Nullable(SeedImage),
|
||||||
|
thumbnail: t.Nullable(SeedImage),
|
||||||
|
banner: t.Nullable(SeedImage),
|
||||||
|
logo: t.Nullable(SeedImage),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
minProperties: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
export type SeedMovie = typeof SeedMovie.static;
|
||||||
|
|
||||||
|
registerExamples(Movie, {
|
||||||
|
...bubble,
|
||||||
|
...bubble.translations.en,
|
||||||
|
...bubbleImages,
|
||||||
|
});
|
||||||
|
registerExamples(SeedMovie, bubble);
|
36
api/src/models/season.ts
Normal file
36
api/src/models/season.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { Image } from "./utils/image";
|
||||||
|
import { SeasonId } from "./utils/external-id";
|
||||||
|
import { Resource } from "./utils/resource";
|
||||||
|
import { Language } from "./utils/language";
|
||||||
|
|
||||||
|
export const BaseSeason = t.Object({
|
||||||
|
seasonNumber: t.Number({ minimum: 1 }),
|
||||||
|
startAir: t.Nullable(t.String({ format: "date" })),
|
||||||
|
endAir: t.Nullable(t.String({ format: "date" })),
|
||||||
|
|
||||||
|
createdAt: t.String({ format: "date-time" }),
|
||||||
|
nextRefresh: t.String({ format: "date-time" }),
|
||||||
|
|
||||||
|
externalId: SeasonId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SeasonTranslation = t.Object({
|
||||||
|
name: t.Nullable(t.String()),
|
||||||
|
description: t.Nullable(t.String()),
|
||||||
|
|
||||||
|
poster: t.Nullable(Image),
|
||||||
|
thumbnail: t.Nullable(Image),
|
||||||
|
banner: t.Nullable(Image),
|
||||||
|
});
|
||||||
|
export type SeasonTranslation = typeof SeasonTranslation.static;
|
||||||
|
|
||||||
|
export const Season = t.Intersect([Resource, BaseSeason, SeasonTranslation]);
|
||||||
|
export type Season = typeof Season.static;
|
||||||
|
|
||||||
|
export const SeedSeason = t.Intersect([
|
||||||
|
BaseSeason,
|
||||||
|
t.Object({
|
||||||
|
translations: t.Record(Language(), SeasonTranslation, { minPropreties: 1 }),
|
||||||
|
}),
|
||||||
|
]);
|
70
api/src/models/serie.ts
Normal file
70
api/src/models/serie.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { Genre } from "./utils/genres";
|
||||||
|
import { Image } from "./utils/image";
|
||||||
|
import { ExternalId } from "./utils/external-id";
|
||||||
|
import { madeInAbyss, registerExamples } from "./examples";
|
||||||
|
import { Resource } from "./utils/resource";
|
||||||
|
import { Language } from "./utils/language";
|
||||||
|
import { SeedSeason } from "./season";
|
||||||
|
|
||||||
|
export const SerieStatus = t.UnionEnum([
|
||||||
|
"unknown",
|
||||||
|
"finished",
|
||||||
|
"airing",
|
||||||
|
"planned",
|
||||||
|
]);
|
||||||
|
export type SerieStatus = typeof SerieStatus.static;
|
||||||
|
|
||||||
|
export const BaseSerie = t.Object({
|
||||||
|
genres: t.Array(Genre),
|
||||||
|
rating: t.Nullable(t.Number({ minimum: 0, maximum: 100 })),
|
||||||
|
status: SerieStatus,
|
||||||
|
runtime: t.Nullable(
|
||||||
|
t.Number({
|
||||||
|
minimum: 0,
|
||||||
|
description: "Average runtime of all episodes (in minutes.)",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
startAir: t.Nullable(t.String({ format: "date" })),
|
||||||
|
endAir: t.Nullable(t.String({ format: "date" })),
|
||||||
|
originalLanguage: t.Nullable(
|
||||||
|
Language({
|
||||||
|
description: "The language code this serie was made in.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
createdAt: t.String({ format: "date-time" }),
|
||||||
|
nextRefresh: t.String({ format: "date-time" }),
|
||||||
|
|
||||||
|
externalId: ExternalId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SerieTranslation = t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
description: t.Nullable(t.String()),
|
||||||
|
tagline: t.Nullable(t.String()),
|
||||||
|
aliases: t.Array(t.String()),
|
||||||
|
tags: t.Array(t.String()),
|
||||||
|
|
||||||
|
poster: t.Nullable(Image),
|
||||||
|
thumbnail: t.Nullable(Image),
|
||||||
|
banner: t.Nullable(Image),
|
||||||
|
logo: t.Nullable(Image),
|
||||||
|
trailerUrl: t.Nullable(t.String()),
|
||||||
|
});
|
||||||
|
export type SerieTranslation = typeof SerieTranslation.static;
|
||||||
|
|
||||||
|
export const Serie = t.Intersect([Resource, BaseSerie, SerieTranslation]);
|
||||||
|
export type Serie = typeof Serie.static;
|
||||||
|
|
||||||
|
export const SeedSerie = t.Intersect([
|
||||||
|
BaseSerie,
|
||||||
|
t.Object({
|
||||||
|
translations: t.Record(Language(), SerieTranslation, { minProperties: 1 }),
|
||||||
|
seasons: t.Array(SeedSeason),
|
||||||
|
// entries: t.Array(SeedEntry),
|
||||||
|
// extras: t.Optional(t.Array(SeedExtra)),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
export type SeedSerie = typeof SeedSerie.static;
|
46
api/src/models/utils/external-id.ts
Normal file
46
api/src/models/utils/external-id.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "../../utils";
|
||||||
|
|
||||||
|
export const ExternalId = t.Record(
|
||||||
|
t.String(),
|
||||||
|
t.Object({
|
||||||
|
dataId: t.String(),
|
||||||
|
link: t.Nullable(t.String({ format: "uri" })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export type ExternalId = typeof ExternalId.static;
|
||||||
|
|
||||||
|
export const EpisodeId = t.Record(
|
||||||
|
t.String(),
|
||||||
|
t.Object({
|
||||||
|
serieId: t.String({
|
||||||
|
descrpition: comment`
|
||||||
|
Id on the external website.
|
||||||
|
We store the serie's id because episode id are rarely stable.
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
season: t.Nullable(
|
||||||
|
t.Number({
|
||||||
|
description: "Null if the external website uses absolute numbering.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
episode: t.Number(),
|
||||||
|
link: t.Nullable(t.String({ format: "uri" })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export type EpisodeId = typeof EpisodeId.static;
|
||||||
|
|
||||||
|
export const SeasonId = t.Record(
|
||||||
|
t.String(),
|
||||||
|
t.Object({
|
||||||
|
serieId: t.String({
|
||||||
|
descrpition: comment`
|
||||||
|
Id on the external website.
|
||||||
|
We store the serie's id because episode id are rarely stable.
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
season: t.Number(),
|
||||||
|
link: t.Nullable(t.String({ format: "uri" })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export type SeasonId = typeof SeasonId.static;
|
49
api/src/models/utils/filters/index.ts
Normal file
49
api/src/models/utils/filters/index.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import type { Column } from "drizzle-orm";
|
||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "~/utils";
|
||||||
|
import { expression } from "./parser";
|
||||||
|
import { toDrizzle } from "./to-sql";
|
||||||
|
import { KErrorT } from "~/models/error";
|
||||||
|
|
||||||
|
export type FilterDef = {
|
||||||
|
[key: string]:
|
||||||
|
| {
|
||||||
|
column: Column;
|
||||||
|
type: "int" | "float" | "date" | "string";
|
||||||
|
isArray?: boolean;
|
||||||
|
}
|
||||||
|
| { column: Column; type: "enum"; values: string[]; isArray?: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Filter = ({
|
||||||
|
def,
|
||||||
|
description = "Filters to apply to the query.",
|
||||||
|
}: { def: FilterDef; description?: string }) =>
|
||||||
|
t
|
||||||
|
.Transform(
|
||||||
|
t.String({
|
||||||
|
description: comment`
|
||||||
|
${description}
|
||||||
|
|
||||||
|
This is based on [odata's filter specification](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter).
|
||||||
|
Filters available: ${Object.keys(def).join(", ")}.
|
||||||
|
`,
|
||||||
|
example: "(rating gt 75 and genres has action) or status eq planned",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.Decode((filter) => {
|
||||||
|
return parseFilters(filter, def);
|
||||||
|
})
|
||||||
|
.Encode(() => {
|
||||||
|
throw new Error("Can't encode filters");
|
||||||
|
});
|
||||||
|
|
||||||
|
export const parseFilters = (filter: string | undefined, config: FilterDef) => {
|
||||||
|
if (!filter) return undefined;
|
||||||
|
const ret = expression.parse(filter);
|
||||||
|
if (!ret.isOk) {
|
||||||
|
throw new KErrorT(`Invalid filter: ${filter}.`, ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toDrizzle(ret.value, config);
|
||||||
|
};
|
130
api/src/models/utils/filters/parser.ts
Normal file
130
api/src/models/utils/filters/parser.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
anyStringOf,
|
||||||
|
digit,
|
||||||
|
float,
|
||||||
|
int,
|
||||||
|
letter,
|
||||||
|
noCharOf,
|
||||||
|
type Parjser,
|
||||||
|
string,
|
||||||
|
} from "parjs";
|
||||||
|
import {
|
||||||
|
exactly,
|
||||||
|
many,
|
||||||
|
many1,
|
||||||
|
map,
|
||||||
|
or,
|
||||||
|
stringify,
|
||||||
|
then,
|
||||||
|
thenq,
|
||||||
|
qthen,
|
||||||
|
later,
|
||||||
|
between,
|
||||||
|
recover,
|
||||||
|
} from "parjs/combinators";
|
||||||
|
|
||||||
|
export type Property = string;
|
||||||
|
export type Value =
|
||||||
|
| { type: "int"; value: number }
|
||||||
|
| { type: "float"; value: number }
|
||||||
|
| { type: "date"; value: string }
|
||||||
|
| { type: "string"; value: string }
|
||||||
|
| { type: "enum"; value: string };
|
||||||
|
const operators = ["eq", "ne", "gt", "ge", "lt", "le", "has"] as const;
|
||||||
|
export type Operator = (typeof operators)[number];
|
||||||
|
export type Expression =
|
||||||
|
| { type: "op"; operator: Operator; property: Property; value: Value }
|
||||||
|
| { type: "and"; lhs: Expression; rhs: Expression }
|
||||||
|
| { type: "or"; lhs: Expression; rhs: Expression }
|
||||||
|
| { type: "not"; expression: Expression };
|
||||||
|
|
||||||
|
function t<T>(parser: Parjser<T>): Parjser<T> {
|
||||||
|
return parser.pipe(thenq(string(" ").pipe(many())));
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = t(noCharOf(" ").pipe(many1(), stringify()).expects("a string"));
|
||||||
|
const enumP = t(letter().pipe(many1(), stringify()).expects("an enum value"));
|
||||||
|
|
||||||
|
const property = str.expects("a property");
|
||||||
|
|
||||||
|
const intVal = t(int().pipe(map((i) => ({ type: "int" as const, value: i }))));
|
||||||
|
const floatVal = t(
|
||||||
|
float().pipe(map((f) => ({ type: "float" as const, value: f }))),
|
||||||
|
);
|
||||||
|
const dateVal = t(
|
||||||
|
digit(10).pipe(
|
||||||
|
exactly(4),
|
||||||
|
stringify(),
|
||||||
|
thenq(string("-")),
|
||||||
|
then(
|
||||||
|
digit(10).pipe(exactly(2), stringify(), thenq(string("-"))),
|
||||||
|
digit(10).pipe(exactly(2), stringify()),
|
||||||
|
),
|
||||||
|
map(([year, month, day]) => ({
|
||||||
|
type: "date" as const,
|
||||||
|
value: `${year}-${month}-${day}`,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
).expects("a date");
|
||||||
|
const strVal = str.pipe(
|
||||||
|
between('"'),
|
||||||
|
or(str.pipe(between("'"))),
|
||||||
|
map((s) => ({ type: "string" as const, value: s })),
|
||||||
|
);
|
||||||
|
const enumVal = enumP.pipe(map((e) => ({ type: "enum" as const, value: e })));
|
||||||
|
const value = dateVal
|
||||||
|
.pipe(
|
||||||
|
// until we get the `-` character, this could be an int or a float.
|
||||||
|
recover(() => ({ kind: "Soft" })),
|
||||||
|
or(intVal, floatVal, strVal, enumVal),
|
||||||
|
)
|
||||||
|
.expects("a valid value");
|
||||||
|
|
||||||
|
const operator = t(anyStringOf(...operators)).expects("an operator");
|
||||||
|
|
||||||
|
export const operation = property
|
||||||
|
.pipe(
|
||||||
|
then(operator, value),
|
||||||
|
map(([property, operator, value]) => ({
|
||||||
|
type: "op" as const,
|
||||||
|
property,
|
||||||
|
operator,
|
||||||
|
value,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.expects("an operation");
|
||||||
|
|
||||||
|
// grammar:
|
||||||
|
//
|
||||||
|
// operation = property operator value
|
||||||
|
// property = letter { letter }
|
||||||
|
// operator = "eq" | "lt" | ...
|
||||||
|
// value = ...
|
||||||
|
//
|
||||||
|
// expression = expr { binn expr }
|
||||||
|
// expr =
|
||||||
|
// | "not" expr
|
||||||
|
// | "(" expression ")"
|
||||||
|
// | operation
|
||||||
|
// bin = "and" | "or"
|
||||||
|
//
|
||||||
|
const expr = later<Expression>();
|
||||||
|
|
||||||
|
export const expression = expr.pipe(
|
||||||
|
then(t(anyStringOf("and", "or")).pipe(then(expr), many())),
|
||||||
|
map(([first, expr]) =>
|
||||||
|
expr.reduce<Expression>(
|
||||||
|
(lhs, [op, rhs]) => ({ type: op, lhs, rhs }),
|
||||||
|
first,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const not = t(string("not")).pipe(
|
||||||
|
qthen(expr),
|
||||||
|
map((expression) => ({ type: "not" as const, expression })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const brackets = expression.pipe(between("(", ")"));
|
||||||
|
|
||||||
|
expr.init(not.pipe(or(brackets, operation)));
|
103
api/src/models/utils/filters/to-sql.ts
Normal file
103
api/src/models/utils/filters/to-sql.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
gt,
|
||||||
|
gte,
|
||||||
|
lt,
|
||||||
|
lte,
|
||||||
|
ne,
|
||||||
|
not,
|
||||||
|
or,
|
||||||
|
type SQL,
|
||||||
|
sql,
|
||||||
|
} from "drizzle-orm";
|
||||||
|
import { comment } from "~/utils";
|
||||||
|
import type { FilterDef } from "./index";
|
||||||
|
import type { Expression, Operator } from "./parser";
|
||||||
|
import { KErrorT } from "~/models/error";
|
||||||
|
|
||||||
|
const opMap: Record<Operator, typeof eq> = {
|
||||||
|
eq: eq,
|
||||||
|
ne: ne,
|
||||||
|
gt: gt,
|
||||||
|
ge: gte,
|
||||||
|
lt: lt,
|
||||||
|
le: lte,
|
||||||
|
has: eq,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toDrizzle = (expr: Expression, config: FilterDef): SQL => {
|
||||||
|
switch (expr.type) {
|
||||||
|
case "op": {
|
||||||
|
const where = `${expr.property} ${expr.operator} ${expr.value.value}`;
|
||||||
|
const prop = config[expr.property];
|
||||||
|
|
||||||
|
if (!prop) {
|
||||||
|
throw new KErrorT(
|
||||||
|
comment`
|
||||||
|
Invalid property: ${expr.property}.
|
||||||
|
Expected one of ${Object.keys(config).join(", ")}.
|
||||||
|
`,
|
||||||
|
{ in: where },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop.type !== expr.value.type) {
|
||||||
|
throw new KErrorT(
|
||||||
|
comment`
|
||||||
|
Invalid value for property ${expr.property}.
|
||||||
|
Got ${expr.value.type} but expected ${prop.type}.
|
||||||
|
`,
|
||||||
|
{ in: where },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
prop.type === "enum" &&
|
||||||
|
(expr.value.type === "enum" || expr.value.type === "string") &&
|
||||||
|
!prop.values.includes(expr.value.value)
|
||||||
|
) {
|
||||||
|
throw new KErrorT(
|
||||||
|
comment`
|
||||||
|
Invalid value ${expr.value.value} for property ${expr.property}.
|
||||||
|
Expected one of ${prop.values.join(", ")} but got ${expr.value.value}.
|
||||||
|
`,
|
||||||
|
{ in: where },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop.isArray) {
|
||||||
|
if (expr.operator !== "has" && expr.operator !== "eq") {
|
||||||
|
throw new KErrorT(
|
||||||
|
comment`
|
||||||
|
Property ${expr.property} is an array but you wanted to use the
|
||||||
|
operator ${expr.operator}. Only "has" is supported ("eq" is also aliased to "has")
|
||||||
|
`,
|
||||||
|
{ in: where },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sql`${expr.value.value} = any(${prop.column})`;
|
||||||
|
}
|
||||||
|
return opMap[expr.operator](prop.column, expr.value.value);
|
||||||
|
}
|
||||||
|
case "and": {
|
||||||
|
const lhs = toDrizzle(expr.lhs, config);
|
||||||
|
const rhs = toDrizzle(expr.rhs, config);
|
||||||
|
return and(lhs, rhs)!;
|
||||||
|
}
|
||||||
|
case "or": {
|
||||||
|
const lhs = toDrizzle(expr.lhs, config);
|
||||||
|
const rhs = toDrizzle(expr.rhs, config);
|
||||||
|
return or(lhs, rhs)!;
|
||||||
|
}
|
||||||
|
case "not": {
|
||||||
|
const lhs = toDrizzle(expr.expression, config);
|
||||||
|
return not(lhs);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return exhaustiveCheck(expr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function exhaustiveCheck(v: never): never {
|
||||||
|
return v;
|
||||||
|
}
|
28
api/src/models/utils/genres.ts
Normal file
28
api/src/models/utils/genres.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
|
export const Genre = t.UnionEnum([
|
||||||
|
"action",
|
||||||
|
"adventure",
|
||||||
|
"animation",
|
||||||
|
"comedy",
|
||||||
|
"crime",
|
||||||
|
"documentary",
|
||||||
|
"drama",
|
||||||
|
"family",
|
||||||
|
"fantasy",
|
||||||
|
"history",
|
||||||
|
"horror",
|
||||||
|
"music",
|
||||||
|
"mystery",
|
||||||
|
"romance",
|
||||||
|
"science-fiction",
|
||||||
|
"thriller",
|
||||||
|
"war",
|
||||||
|
"western",
|
||||||
|
"kids",
|
||||||
|
"reality",
|
||||||
|
"politics",
|
||||||
|
"soap",
|
||||||
|
"talk",
|
||||||
|
]);
|
||||||
|
export type Genre = typeof Genre.static;
|
10
api/src/models/utils/image.ts
Normal file
10
api/src/models/utils/image.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
|
export const Image = t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
source: t.String({ format: "uri" }),
|
||||||
|
blurhash: t.String(),
|
||||||
|
});
|
||||||
|
export type Image = typeof Image.static;
|
||||||
|
|
||||||
|
export const SeedImage = t.String({ format: "uri" });
|
9
api/src/models/utils/index.ts
Normal file
9
api/src/models/utils/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export * from "./external-id";
|
||||||
|
export * from "./genres";
|
||||||
|
export * from "./image";
|
||||||
|
export * from "./language";
|
||||||
|
export * from "./resource";
|
||||||
|
export * from "./filters";
|
||||||
|
export * from "./page";
|
||||||
|
export * from "./sort";
|
||||||
|
export * from "./keyset-paginate";
|
69
api/src/models/utils/keyset-paginate.ts
Normal file
69
api/src/models/utils/keyset-paginate.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import type { NonEmptyArray, Sort } from "./sort";
|
||||||
|
import { eq, or, type Column, and, gt, lt, isNull } from "drizzle-orm";
|
||||||
|
|
||||||
|
type Table<Name extends string> = Record<Name, Column>;
|
||||||
|
|
||||||
|
type After = (string | number | boolean | undefined)[];
|
||||||
|
|
||||||
|
// Create a filter (where) expression on the query to skip everything before/after the referenceID.
|
||||||
|
// The generalized expression for this in pseudocode is:
|
||||||
|
// (x > a) OR
|
||||||
|
// (x = a AND y > b) OR
|
||||||
|
// (x = a AND y = b AND z > c) OR...
|
||||||
|
//
|
||||||
|
// Of course, this will be a bit more complex when ASC and DESC are mixed.
|
||||||
|
// Assume x is ASC, y is DESC, and z is ASC:
|
||||||
|
// (x > a) OR
|
||||||
|
// (x = a AND y < b) OR
|
||||||
|
// (x = a AND y = b AND z > c) OR...
|
||||||
|
export const keysetPaginate = <
|
||||||
|
const T extends NonEmptyArray<string>,
|
||||||
|
const Remap extends Partial<Record<T[number], string>>,
|
||||||
|
>({
|
||||||
|
table,
|
||||||
|
sort,
|
||||||
|
after,
|
||||||
|
}: {
|
||||||
|
table: Table<"pk" | Sort<T, Remap>[number]["key"]>;
|
||||||
|
after: string | undefined;
|
||||||
|
sort: Sort<T, Remap>;
|
||||||
|
}) => {
|
||||||
|
if (!after) return undefined;
|
||||||
|
const cursor: After = JSON.parse(
|
||||||
|
Buffer.from(after, "base64").toString("utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pkSort = { key: "pk" as const, desc: false };
|
||||||
|
|
||||||
|
// TODO: Add an outer query >= for perf
|
||||||
|
// PERF: See https://use-the-index-luke.com/sql/partial-results/fetch-next-page#sb-equivalent-logic
|
||||||
|
let where = undefined;
|
||||||
|
let previous = undefined;
|
||||||
|
for (const [i, by] of [...sort, pkSort].entries()) {
|
||||||
|
const cmp = by.desc ? lt : gt;
|
||||||
|
where = or(
|
||||||
|
where,
|
||||||
|
and(
|
||||||
|
previous,
|
||||||
|
or(
|
||||||
|
cmp(table[by.key], cursor[i]),
|
||||||
|
!table[by.key].notNull ? isNull(table[by.key]) : undefined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
previous = and(
|
||||||
|
previous,
|
||||||
|
cursor[i] === null ? isNull(table[by.key]) : eq(table[by.key], cursor[i]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateAfter = (cursor: any, sort: Sort<any, any>) => {
|
||||||
|
const ret = [
|
||||||
|
...sort.map((by) => cursor[by.remmapedKey ?? by.key]),
|
||||||
|
cursor.pk,
|
||||||
|
];
|
||||||
|
return Buffer.from(JSON.stringify(ret), "utf-8").toString("base64url");
|
||||||
|
};
|
66
api/src/models/utils/language.ts
Normal file
66
api/src/models/utils/language.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { FormatRegistry } from "@sinclair/typebox";
|
||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "../../utils";
|
||||||
|
import type { KError } from "../error";
|
||||||
|
|
||||||
|
export const validateTranslations = <T extends object>(
|
||||||
|
translations: Record<string, T>,
|
||||||
|
): KError | null => {
|
||||||
|
for (const lang of Object.keys(translations)) {
|
||||||
|
try {
|
||||||
|
const valid = new Intl.Locale(lang).baseName;
|
||||||
|
if (lang !== valid) {
|
||||||
|
translations[valid] = translations[lang];
|
||||||
|
delete translations[lang];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
message: `Invalid translation name: '${lang}'.`,
|
||||||
|
details: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
FormatRegistry.Set("language", (lang) => {
|
||||||
|
try {
|
||||||
|
const normalized = new Intl.Locale(lang).baseName;
|
||||||
|
// TODO: we should actually replace the locale with normalized if we managed to parse it but transforms aren't working
|
||||||
|
return lang === normalized;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type StringProps = NonNullable<Parameters<typeof t.String>[0]>;
|
||||||
|
|
||||||
|
// TODO: format validation doesn't work in record's key. We should have a proper way to check that.
|
||||||
|
export const Language = (props?: StringProps) =>
|
||||||
|
t.String({
|
||||||
|
format: "language",
|
||||||
|
description: comment`
|
||||||
|
${props?.description ?? ""}
|
||||||
|
This is a BCP 47 language code (the IETF Best Current Practices on Tags for Identifying Languages).
|
||||||
|
BCP 47 is also known as RFC 5646. It subsumes ISO 639 and is backward compatible with it.
|
||||||
|
`,
|
||||||
|
error: "Expected a valid (and NORMALIZED) bcp-47 language code.",
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const processLanguages = (languages?: string) => {
|
||||||
|
if (!languages) return ["*"];
|
||||||
|
return languages
|
||||||
|
.split(",")
|
||||||
|
.map((x) => {
|
||||||
|
const [lang, q] = x.trim().split(";q=");
|
||||||
|
return [lang, q ? Number.parseFloat(q) : 1] as const;
|
||||||
|
})
|
||||||
|
.sort(([_, q1], [__, q2]) => q1 - q2)
|
||||||
|
.flatMap(([lang]) => {
|
||||||
|
const [base, spec] = lang.split("-");
|
||||||
|
if (spec) return [lang, base];
|
||||||
|
return [lang];
|
||||||
|
});
|
||||||
|
};
|
31
api/src/models/utils/page.ts
Normal file
31
api/src/models/utils/page.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { ObjectOptions } from "@sinclair/typebox";
|
||||||
|
import { t, type TSchema } from "elysia";
|
||||||
|
import type { Sort } from "./sort";
|
||||||
|
import { generateAfter } from "./keyset-paginate";
|
||||||
|
|
||||||
|
export const Page = <T extends TSchema>(schema: T, options?: ObjectOptions) =>
|
||||||
|
t.Object(
|
||||||
|
{
|
||||||
|
items: t.Array(schema),
|
||||||
|
this: t.String({ format: "uri" }),
|
||||||
|
next: t.Nullable(t.String({ format: "uri" })),
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createPage = <T>(
|
||||||
|
items: T[],
|
||||||
|
{ url, sort, limit }: { url: string; sort: Sort<any, any>; limit: number },
|
||||||
|
) => {
|
||||||
|
let next: string | null = null;
|
||||||
|
|
||||||
|
// we can't know for sure if there's a next page when the current page is full.
|
||||||
|
// maybe the next page is empty, this is a bit weird but it allows us to handle pages
|
||||||
|
// without making a new request to the db so it's fine.
|
||||||
|
if (items.length === limit && limit > 0) {
|
||||||
|
const uri = new URL(url);
|
||||||
|
uri.searchParams.set("after", generateAfter(items[items.length - 1], sort));
|
||||||
|
next = uri.toString();
|
||||||
|
}
|
||||||
|
return { items, this: url, next };
|
||||||
|
};
|
17
api/src/models/utils/resource.ts
Normal file
17
api/src/models/utils/resource.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { FormatRegistry } from "@sinclair/typebox";
|
||||||
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
|
export const slugPattern = "^[a-z0-9-]+$";
|
||||||
|
|
||||||
|
FormatRegistry.Set("slug", (slug) => {
|
||||||
|
return /^[a-z0-9-]+$/g.test(slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Resource = t.Object({
|
||||||
|
id: t.String({ format: "uuid" }),
|
||||||
|
slug: t.String({ format: "slug" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const checker = TypeCompiler.Compile(t.String({ format: "uuid" }));
|
||||||
|
export const isUuid = (id: string) => checker.Check(id);
|
54
api/src/models/utils/sort.ts
Normal file
54
api/src/models/utils/sort.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
|
export type Sort<
|
||||||
|
T extends string[],
|
||||||
|
Remap extends Partial<Record<T[number], string>>,
|
||||||
|
> = {
|
||||||
|
key: Exclude<T[number], keyof Remap> | NonNullable<Remap[keyof Remap]>;
|
||||||
|
remmapedKey?: keyof Remap;
|
||||||
|
desc: boolean;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
|
export const Sort = <
|
||||||
|
const T extends NonEmptyArray<string>,
|
||||||
|
const Remap extends Partial<Record<T[number], string>>,
|
||||||
|
>(
|
||||||
|
values: T,
|
||||||
|
{
|
||||||
|
description = "How to sort the query",
|
||||||
|
default: def,
|
||||||
|
remap,
|
||||||
|
}: {
|
||||||
|
default?: T[number][];
|
||||||
|
description: string;
|
||||||
|
remap: Remap;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
t
|
||||||
|
.Transform(
|
||||||
|
t.Array(
|
||||||
|
t.UnionEnum([
|
||||||
|
...values,
|
||||||
|
...values.map((x: T[number]) => `-${x}` as const),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
// TODO: support explode: true (allow sort=slug,-createdAt). needs a pr to elysia
|
||||||
|
explode: false,
|
||||||
|
default: def,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.Decode((sort): Sort<T, Remap> => {
|
||||||
|
return sort.map((x) => {
|
||||||
|
const desc = x[0] === "-";
|
||||||
|
const key = (desc ? x.substring(1) : x) as T[number];
|
||||||
|
if (key in remap) return { key: remap[key]!, remmapedKey: key, desc };
|
||||||
|
return { key: key as Exclude<typeof key, keyof Remap>, desc };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Encode(() => {
|
||||||
|
throw new Error("Encode not supported for sort");
|
||||||
|
});
|
38
api/src/models/video.ts
Normal file
38
api/src/models/video.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "../utils";
|
||||||
|
import { registerExamples, bubbleVideo } from "./examples";
|
||||||
|
|
||||||
|
export const Video = t.Object({
|
||||||
|
id: t.String({ format: "uuid" }),
|
||||||
|
slug: t.String({ format: "slug" }),
|
||||||
|
path: t.String(),
|
||||||
|
rendering: t.String({
|
||||||
|
description: comment`
|
||||||
|
Sha of the path except \`part\` & \`version\`.
|
||||||
|
If there are multiples files for the same entry, it can be used to know if each
|
||||||
|
file is the same content or if it's unrelated (like long-version vs short-version, monochrome vs colored etc)
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
part: t.Nullable(
|
||||||
|
t.Number({
|
||||||
|
minimum: 0,
|
||||||
|
description: comment`
|
||||||
|
If the episode/movie is split into multiples files, the \`part\` field can be used to order them.
|
||||||
|
The \`rendering\` field is used to know if two parts are in the same group or
|
||||||
|
if it's another unrelated video file of the same entry.
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
version: t.Number({
|
||||||
|
minimum: 0,
|
||||||
|
default: 1,
|
||||||
|
description:
|
||||||
|
"Kyoo will prefer playing back the highest `version` number if there are multiples rendering.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
createdAt: t.String({ format: "date-time" }),
|
||||||
|
});
|
||||||
|
export type Video = typeof Video.static;
|
||||||
|
registerExamples(Video, bubbleVideo);
|
||||||
|
|
||||||
|
export const SeedVideo = t.Omit(Video, ["id", "slug", "createdAt"]);
|
8
api/src/utils.ts
Normal file
8
api/src/utils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// remove indent in multi-line comments
|
||||||
|
export const comment = (str: TemplateStringsArray, ...values: any[]) =>
|
||||||
|
str
|
||||||
|
.reduce((acc, str, i) => `${acc}${values[i - 1]}${str}`)
|
||||||
|
.replace(/(^\s)|(\s+$)/g, "") // first & last whitespaces
|
||||||
|
.replace(/^[ \t]+/gm, "") // leading spaces
|
||||||
|
.replace(/([^\n])\n([^\n])/g, "$1 $2") // two lines to space separated line
|
||||||
|
.replace(/\n{2}/g, "\n"); // keep newline if there's an empty line
|
173
api/tests/misc/filter.test.ts
Normal file
173
api/tests/misc/filter.test.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import type { ParjsFailure } from "parjs/internal";
|
||||||
|
import { type Expression, expression } from "~/models/utils/filters/parser";
|
||||||
|
|
||||||
|
function parse(
|
||||||
|
filter: string,
|
||||||
|
): { ok: true; value: Expression } | { ok: false } {
|
||||||
|
const ret = expression.parse(filter);
|
||||||
|
if (ret.isOk) return { ok: true, value: ret.value };
|
||||||
|
const fail = ret as ParjsFailure;
|
||||||
|
console.log(fail.toString());
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: fail.reason,
|
||||||
|
trace: {
|
||||||
|
...fail.trace,
|
||||||
|
location: fail.trace.location,
|
||||||
|
leftover: fail.trace.input.substring(fail.trace.location.column),
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Parse filter", () => {
|
||||||
|
it("Handle eq", () => {
|
||||||
|
const ret = parse("status eq finished");
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "op",
|
||||||
|
operator: "eq",
|
||||||
|
property: "status",
|
||||||
|
value: { type: "enum", value: "finished" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Handle lt", () => {
|
||||||
|
const ret = parse("rating lt 10");
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 10 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Handle dates", () => {
|
||||||
|
const ret = parse("airDate ge 2022-10-12");
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "op",
|
||||||
|
operator: "ge",
|
||||||
|
property: "airDate",
|
||||||
|
value: { type: "date", value: "2022-10-12" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Handle not", () => {
|
||||||
|
const ret = parse("not rating lt 10");
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "not",
|
||||||
|
expression: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Handle top level brackets", () => {
|
||||||
|
const ret = parse("(rating lt 10)");
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 10 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Handle top level brackets with not", () => {
|
||||||
|
const ret = parse("(not rating lt 10)");
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "not",
|
||||||
|
expression: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Handle and", () => {
|
||||||
|
const ret = parse("not rating lt 10 and rating lt 20");
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "and",
|
||||||
|
lhs: {
|
||||||
|
type: "not",
|
||||||
|
expression: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 20 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Handle or", () => {
|
||||||
|
const ret = parse(
|
||||||
|
"not rating lt 10 and rating lt 20 or (status eq finished and not status ne airing)",
|
||||||
|
);
|
||||||
|
expect(ret).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
type: "or",
|
||||||
|
lhs: {
|
||||||
|
type: "and",
|
||||||
|
lhs: {
|
||||||
|
type: "not",
|
||||||
|
expression: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: {
|
||||||
|
type: "op",
|
||||||
|
operator: "lt",
|
||||||
|
property: "rating",
|
||||||
|
value: { type: "int", value: 20 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: {
|
||||||
|
type: "and",
|
||||||
|
lhs: {
|
||||||
|
type: "op",
|
||||||
|
operator: "eq",
|
||||||
|
property: "status",
|
||||||
|
value: { type: "enum", value: "finished" },
|
||||||
|
},
|
||||||
|
rhs: {
|
||||||
|
type: "not",
|
||||||
|
expression: {
|
||||||
|
type: "op",
|
||||||
|
operator: "ne",
|
||||||
|
property: "status",
|
||||||
|
value: { type: "enum", value: "airing" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
146
api/tests/movies/get-all-movies-with-null.test.ts
Normal file
146
api/tests/movies/get-all-movies-with-null.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import { seedMovie } from "~/controllers/seed/movies";
|
||||||
|
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 { eq } from "drizzle-orm";
|
||||||
|
import { expectStatus } from "tests/utils";
|
||||||
|
import { createMovie, getMovies, movieApp } from "./movies-helper";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await db.delete(shows);
|
||||||
|
for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.delete(shows);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with a null value", () => {
|
||||||
|
// Those before/after hooks are NOT scopped to the describe due to a bun bug
|
||||||
|
// instead we just make a new file for those /shrug
|
||||||
|
// see: https://github.com/oven-sh/bun/issues/5738
|
||||||
|
beforeAll(async () => {
|
||||||
|
await createMovie({
|
||||||
|
slug: "no-air-date",
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
name: "no air date",
|
||||||
|
description: null,
|
||||||
|
aliases: [],
|
||||||
|
banner: null,
|
||||||
|
logo: null,
|
||||||
|
poster: null,
|
||||||
|
tagline: null,
|
||||||
|
tags: [],
|
||||||
|
thumbnail: null,
|
||||||
|
trailerUrl: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
genres: [],
|
||||||
|
status: "unknown",
|
||||||
|
rating: null,
|
||||||
|
runtime: null,
|
||||||
|
airDate: null,
|
||||||
|
originalLanguage: null,
|
||||||
|
externalId: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.delete(shows).where(eq(shows.slug, "no-air-date"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sort by dates desc with a null value", async () => {
|
||||||
|
let [resp, body] = await getMovies({
|
||||||
|
limit: 2,
|
||||||
|
sort: "-airDate",
|
||||||
|
langs: "en",
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
|
||||||
|
expect(body.items.map((x: any) => x.slug)).toMatchObject([
|
||||||
|
bubble.slug,
|
||||||
|
dune.slug,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// we copy this due to https://github.com/oven-sh/bun/issues/3521
|
||||||
|
const next = body.next;
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({ slug: bubble.slug, airDate: bubble.airDate }),
|
||||||
|
expect.objectContaining({ slug: dune.slug, airDate: dune.airDate }),
|
||||||
|
],
|
||||||
|
this: "http://localhost/movies?limit=2&sort=-airDate",
|
||||||
|
next: expect.stringContaining(
|
||||||
|
"http://localhost/movies?limit=2&sort=-airDate&after=WyIyMDIxLTEwLTIyIiw",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
resp = await movieApp.handle(new Request(next));
|
||||||
|
body = await resp.json();
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.items.map((x: any) => x.slug)).toMatchObject([
|
||||||
|
dune1984.slug,
|
||||||
|
"no-air-date",
|
||||||
|
]);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: dune1984.slug,
|
||||||
|
airDate: dune1984.airDate,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: "no-air-date",
|
||||||
|
airDate: null,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
this: next,
|
||||||
|
next: expect.anything(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("sort by dates asc with a null value", async () => {
|
||||||
|
let [resp, body] = await getMovies({
|
||||||
|
limit: 2,
|
||||||
|
sort: "airDate",
|
||||||
|
langs: "en",
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
|
||||||
|
// we copy this due to https://github.com/oven-sh/bun/issues/3521
|
||||||
|
const next = body.next;
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: dune1984.slug,
|
||||||
|
airDate: dune1984.airDate,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({ slug: dune.slug, airDate: dune.airDate }),
|
||||||
|
],
|
||||||
|
this: "http://localhost/movies?limit=2&sort=airDate",
|
||||||
|
next: expect.stringContaining(
|
||||||
|
"http://localhost/movies?limit=2&sort=airDate&after=WyIyMDIxLTEwLTIyIiw",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
resp = await movieApp.handle(new Request(next));
|
||||||
|
body = await resp.json();
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: bubble.slug,
|
||||||
|
airDate: bubble.airDate,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: "no-air-date",
|
||||||
|
airDate: null,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
this: next,
|
||||||
|
next: expect.anything(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
123
api/tests/movies/get-all-movies.test.ts
Normal file
123
api/tests/movies/get-all-movies.test.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import { expectStatus } from "tests/utils";
|
||||||
|
import { seedMovie } from "~/controllers/seed/movies";
|
||||||
|
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 { getMovies, movieApp } from "./movies-helper";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await db.delete(shows);
|
||||||
|
for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.delete(shows);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Get all movies", () => {
|
||||||
|
it("Invalid filter params", async () => {
|
||||||
|
const [resp, body] = await getMovies({
|
||||||
|
filter: `slug eq ${bubble.slug}`,
|
||||||
|
langs: "en",
|
||||||
|
});
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(422);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
status: 422,
|
||||||
|
message:
|
||||||
|
"Invalid property: slug. Expected one of genres, rating, status, runtime, airDate, originalLanguage.",
|
||||||
|
details: {
|
||||||
|
in: "slug eq bubble",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Invalid filter syntax", async () => {
|
||||||
|
const [resp, body] = await getMovies({
|
||||||
|
filter: `slug eq gt ${bubble.slug}`,
|
||||||
|
langs: "en",
|
||||||
|
});
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(422);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
details: expect.anything(),
|
||||||
|
message: "Invalid filter: slug eq gt bubble.",
|
||||||
|
status: 422,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Limit 2, default sort", async () => {
|
||||||
|
const [resp, body] = await getMovies({
|
||||||
|
limit: 2,
|
||||||
|
langs: "en",
|
||||||
|
});
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({ slug: bubble.slug }),
|
||||||
|
expect.objectContaining({ slug: dune.slug }),
|
||||||
|
],
|
||||||
|
this: "http://localhost/movies?limit=2",
|
||||||
|
// we can't have the exact after since it contains the pk that changes with every tests.
|
||||||
|
next: expect.stringContaining(
|
||||||
|
"http://localhost/movies?limit=2&after=WyJkdW5lIiw",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Limit 2, default sort, page 2", async () => {
|
||||||
|
let [resp, body] = await getMovies({
|
||||||
|
limit: 2,
|
||||||
|
langs: "en",
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
|
||||||
|
resp = await movieApp.handle(new Request(body.next));
|
||||||
|
body = await resp.json();
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [expect.objectContaining({ slug: dune1984.slug })],
|
||||||
|
this: expect.stringContaining(
|
||||||
|
"http://localhost/movies?limit=2&after=WyJkdW5lIiw",
|
||||||
|
),
|
||||||
|
next: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Limit 2, sort by dates desc, page 2", async () => {
|
||||||
|
let [resp, body] = await getMovies({
|
||||||
|
limit: 2,
|
||||||
|
sort: "-airDate",
|
||||||
|
langs: "en",
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
|
||||||
|
// we copy this due to https://github.com/oven-sh/bun/issues/3521
|
||||||
|
const next = body.next;
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({ slug: bubble.slug, airDate: bubble.airDate }),
|
||||||
|
expect.objectContaining({ slug: dune.slug, airDate: dune.airDate }),
|
||||||
|
],
|
||||||
|
this: "http://localhost/movies?limit=2&sort=-airDate",
|
||||||
|
next: expect.stringContaining(
|
||||||
|
"http://localhost/movies?limit=2&sort=-airDate&after=WyIyMDIxLTEwLTIyIiw",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
resp = await movieApp.handle(new Request(next));
|
||||||
|
body = await resp.json();
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: dune1984.slug,
|
||||||
|
airDate: dune1984.airDate,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
this: next,
|
||||||
|
next: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
78
api/tests/movies/get-movie.test.ts
Normal file
78
api/tests/movies/get-movie.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { seedMovie } from "~/controllers/seed/movies";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { shows } from "~/db/schema";
|
||||||
|
import { bubble } from "~/models/examples";
|
||||||
|
import { getMovie } from "./movies-helper";
|
||||||
|
import { expectStatus } from "tests/utils";
|
||||||
|
|
||||||
|
let bubbleId = "";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const ret = await seedMovie(bubble);
|
||||||
|
bubbleId = ret.id;
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.delete(shows).where(eq(shows.slug, bubble.slug));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Get movie", () => {
|
||||||
|
it("Retrive by slug", async () => {
|
||||||
|
const [resp, body] = await getMovie(bubble.slug, "en");
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
slug: bubble.slug,
|
||||||
|
name: bubble.translations.en.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Retrive by id", async () => {
|
||||||
|
const [resp, body] = await getMovie(bubbleId, "en");
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: bubbleId,
|
||||||
|
slug: bubble.slug,
|
||||||
|
name: bubble.translations.en.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Get non available translation", async () => {
|
||||||
|
const [resp, body] = await getMovie(bubble.slug, "fr");
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(422);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
status: 422,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Get first available language", async () => {
|
||||||
|
const [resp, body] = await getMovie(bubble.slug, "fr,en");
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
slug: bubble.slug,
|
||||||
|
name: bubble.translations.en.name,
|
||||||
|
});
|
||||||
|
expect(resp.headers.get("Content-Language")).toBe("en");
|
||||||
|
});
|
||||||
|
it("Use language fallback", async () => {
|
||||||
|
const [resp, body] = await getMovie(bubble.slug, "fr,ja,*");
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
slug: bubble.slug,
|
||||||
|
name: bubble.translations.en.name,
|
||||||
|
});
|
||||||
|
expect(resp.headers.get("Content-Language")).toBe("en");
|
||||||
|
});
|
||||||
|
it("Works without accept-language header", async () => {
|
||||||
|
const [resp, body] = await getMovie(bubble.slug, undefined);
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
slug: bubble.slug,
|
||||||
|
name: bubble.translations.en.name,
|
||||||
|
});
|
||||||
|
expect(resp.headers.get("Content-Language")).toBe("en");
|
||||||
|
});
|
||||||
|
});
|
61
api/tests/movies/movies-helper.ts
Normal file
61
api/tests/movies/movies-helper.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import Elysia from "elysia";
|
||||||
|
import { buildUrl } from "tests/utils";
|
||||||
|
import { base } from "~/base";
|
||||||
|
import { movies } from "~/controllers/movies";
|
||||||
|
import { seed } from "~/controllers/seed";
|
||||||
|
import type { SeedMovie } from "~/models/movie";
|
||||||
|
|
||||||
|
export const movieApp = new Elysia().use(base).use(movies).use(seed);
|
||||||
|
|
||||||
|
export const getMovie = async (id: string, langs?: string) => {
|
||||||
|
const resp = await movieApp.handle(
|
||||||
|
new Request(`http://localhost/movies/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: langs
|
||||||
|
? {
|
||||||
|
"Accept-Language": langs,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMovies = async ({
|
||||||
|
langs,
|
||||||
|
...query
|
||||||
|
}: {
|
||||||
|
filter?: string;
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
sort?: string | string[];
|
||||||
|
langs?: string;
|
||||||
|
}) => {
|
||||||
|
const resp = await movieApp.handle(
|
||||||
|
new Request(buildUrl("movies", query), {
|
||||||
|
method: "GET",
|
||||||
|
headers: langs
|
||||||
|
? {
|
||||||
|
"Accept-Language": langs,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMovie = async (movie: SeedMovie) => {
|
||||||
|
const resp = await movieApp.handle(
|
||||||
|
new Request("http://localhost/movies", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(movie),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
206
api/tests/movies/seed-movies.test.ts
Normal file
206
api/tests/movies/seed-movies.test.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
|
||||||
|
import { eq, inArray } from "drizzle-orm";
|
||||||
|
import { expectStatus } from "tests/utils";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { shows, showTranslations, videos } from "~/db/schema";
|
||||||
|
import { bubble } from "~/models/examples";
|
||||||
|
import { dune, duneVideo } from "~/models/examples/dune-2021";
|
||||||
|
import { createMovie } from "./movies-helper";
|
||||||
|
|
||||||
|
describe("Movie seeding", () => {
|
||||||
|
it("Can create a movie", async () => {
|
||||||
|
// create video beforehand to test linking
|
||||||
|
await db.insert(videos).values(duneVideo);
|
||||||
|
|
||||||
|
const [resp, body] = await createMovie(dune);
|
||||||
|
expectStatus(resp, body).toBe(201);
|
||||||
|
expect(body.id).toBeString();
|
||||||
|
expect(body.slug).toBe("dune");
|
||||||
|
expect(body.videos).toContainEqual({ slug: "dune" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Update existing movie", async () => {
|
||||||
|
// confirm that db is in the correct state (from previous tests)
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(shows)
|
||||||
|
.where(eq(shows.slug, dune.slug))
|
||||||
|
.limit(1);
|
||||||
|
expect(existing).toMatchObject({ slug: dune.slug, startAir: dune.airDate });
|
||||||
|
|
||||||
|
const [resp, body] = await createMovie({
|
||||||
|
...dune,
|
||||||
|
runtime: 200_000,
|
||||||
|
translations: {
|
||||||
|
...dune.translations,
|
||||||
|
en: { ...dune.translations.en, description: "edited translation" },
|
||||||
|
fr: {
|
||||||
|
name: "dune-but-in-french",
|
||||||
|
description: null,
|
||||||
|
tagline: null,
|
||||||
|
aliases: [],
|
||||||
|
tags: [],
|
||||||
|
poster: null,
|
||||||
|
thumbnail: null,
|
||||||
|
banner: null,
|
||||||
|
logo: null,
|
||||||
|
trailerUrl: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [edited] = await db
|
||||||
|
.select()
|
||||||
|
.from(shows)
|
||||||
|
.where(eq(shows.slug, dune.slug))
|
||||||
|
.limit(1);
|
||||||
|
const translations = await db
|
||||||
|
.select()
|
||||||
|
.from(showTranslations)
|
||||||
|
.where(eq(showTranslations.pk, edited.pk));
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.id).toBeString();
|
||||||
|
expect(body.slug).toBe("dune");
|
||||||
|
expect(body.videos).toBeArrayOfSize(0);
|
||||||
|
expect(edited.runtime).toBe(200_000);
|
||||||
|
expect(edited.status).toBe(dune.status);
|
||||||
|
expect(translations.find((x) => x.language === "en")).toMatchObject({
|
||||||
|
name: dune.translations.en.name,
|
||||||
|
description: "edited translation",
|
||||||
|
});
|
||||||
|
expect(translations.find((x) => x.language === "fr")).toMatchObject({
|
||||||
|
name: "dune-but-in-french",
|
||||||
|
description: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Conflicting slug auto-correct", async () => {
|
||||||
|
// confirm that db is in the correct state (from previous tests)
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(shows)
|
||||||
|
.where(eq(shows.slug, dune.slug))
|
||||||
|
.limit(1);
|
||||||
|
expect(existing).toMatchObject({ slug: dune.slug, startAir: dune.airDate });
|
||||||
|
|
||||||
|
const [resp, body] = await createMovie({ ...dune, airDate: "2158-12-13" });
|
||||||
|
expectStatus(resp, body).toBe(201);
|
||||||
|
expect(body.id).toBeString();
|
||||||
|
expect(body.slug).toBe("dune-2158");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Conflict in slug w/out year fails", async () => {
|
||||||
|
// confirm that db is in the correct state (from conflict auto-correct test)
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(shows)
|
||||||
|
.where(eq(shows.slug, dune.slug))
|
||||||
|
.limit(1);
|
||||||
|
expect(existing).toMatchObject({ slug: dune.slug, startAir: dune.airDate });
|
||||||
|
|
||||||
|
const [resp, body] = await createMovie({ ...dune, airDate: null });
|
||||||
|
expectStatus(resp, body).toBe(409);
|
||||||
|
expect(body.id).toBe(existing.id);
|
||||||
|
expect(body.slug).toBe(existing.slug);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Missing videos send info", async () => {
|
||||||
|
const vid = "a0ddf0ce-3258-4452-a670-aff36c76d524";
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(videos)
|
||||||
|
.where(eq(videos.id, vid))
|
||||||
|
.limit(1);
|
||||||
|
expect(existing).toBeUndefined();
|
||||||
|
|
||||||
|
const [resp, body] = await createMovie({
|
||||||
|
...dune,
|
||||||
|
videos: [vid],
|
||||||
|
});
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.videos).toBeArrayOfSize(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Schema error (missing fields)", async () => {
|
||||||
|
const [resp, body] = await createMovie({
|
||||||
|
name: "dune",
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(422);
|
||||||
|
expect(body.status).toBe(422);
|
||||||
|
expect(body.message).toBeString();
|
||||||
|
expect(body.details).toBeObject();
|
||||||
|
// TODO: handle additional fields too
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Invalid translation name", async () => {
|
||||||
|
const [resp, body] = await createMovie({
|
||||||
|
...dune,
|
||||||
|
translations: {
|
||||||
|
...dune.translations,
|
||||||
|
test: {
|
||||||
|
name: "foo",
|
||||||
|
description: "bar",
|
||||||
|
tags: [],
|
||||||
|
aliases: [],
|
||||||
|
tagline: "toto",
|
||||||
|
banner: null,
|
||||||
|
poster: null,
|
||||||
|
thumbnail: null,
|
||||||
|
logo: null,
|
||||||
|
trailerUrl: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(400);
|
||||||
|
expect(body.status).toBe(400);
|
||||||
|
expect(body.message).toBe("Invalid translation name: 'test'.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Correct translations casing.", async () => {
|
||||||
|
const [resp, body] = await createMovie({
|
||||||
|
...bubble,
|
||||||
|
slug: "casing-test",
|
||||||
|
translations: {
|
||||||
|
"en-us": {
|
||||||
|
name: "foo",
|
||||||
|
description: "bar",
|
||||||
|
tags: [],
|
||||||
|
aliases: [],
|
||||||
|
tagline: "toto",
|
||||||
|
banner: null,
|
||||||
|
poster: null,
|
||||||
|
thumbnail: null,
|
||||||
|
logo: null,
|
||||||
|
trailerUrl: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.status).toBeWithin(200, 299);
|
||||||
|
expect(body.id).toBeString();
|
||||||
|
const ret = await db.query.shows.findFirst({
|
||||||
|
where: eq(shows.id, body.id),
|
||||||
|
with: { translations: true },
|
||||||
|
});
|
||||||
|
expect(ret!.translations).toBeArrayOfSize(1);
|
||||||
|
expect(ret!.translations[0]).toMatchObject({
|
||||||
|
language: "en-US",
|
||||||
|
name: "foo",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.todo("Create correct video slug (version)", async () => {});
|
||||||
|
test.todo("Create correct video slug (part)", async () => {});
|
||||||
|
test.todo("Create correct video slug (rendering)", async () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
await db.delete(shows).where(inArray(shows.slug, [dune.slug, "dune-2158"]));
|
||||||
|
await db.delete(videos).where(inArray(videos.id, [duneVideo.id]));
|
||||||
|
};
|
||||||
|
// cleanup db beforehand to unsure tests are consistent
|
||||||
|
beforeAll(cleanup);
|
||||||
|
afterAll(cleanup);
|
10
api/tests/setup.ts
Normal file
10
api/tests/setup.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { beforeAll } from "bun:test";
|
||||||
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||||
|
import { db } from "~/db";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await migrate(db, {
|
||||||
|
migrationsSchema: "kyoo",
|
||||||
|
migrationsFolder: "./drizzle",
|
||||||
|
});
|
||||||
|
});
|
23
api/tests/utils.ts
Normal file
23
api/tests/utils.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { expect } from "bun:test";
|
||||||
|
import Elysia from "elysia";
|
||||||
|
|
||||||
|
export function expectStatus(resp: Response, body: object) {
|
||||||
|
const matcher = expect({ ...body, status: resp.status });
|
||||||
|
return {
|
||||||
|
toBe: (status: number) => {
|
||||||
|
matcher.toMatchObject({ status: status });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildUrl = (route: string, query: Record<string, any>) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(query)) {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
params.append(key, value.toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const v of value) params.append(key, v.toString());
|
||||||
|
}
|
||||||
|
return `http://localhost/${route}?${params}`;
|
||||||
|
};
|
21
api/tsconfig.json
Normal file
21
api/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"types": [
|
||||||
|
"bun-types"
|
||||||
|
],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noErrorTruncation": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user