feat: Add unit tests for resolvers and services, update dependencies, and remove unused scripts.

This commit is contained in:
2025-12-07 05:18:23 -05:00
parent 83732913f7
commit 4b3354a5d6
22 changed files with 1204 additions and 275 deletions

View File

@@ -7,17 +7,15 @@
"scripts": { "scripts": {
"dev": "wrangler dev src/index.ts --port 8080", "dev": "wrangler dev src/index.ts --port 8080",
"deploy": "wrangler deploy --minify src/index.ts", "deploy": "wrangler deploy --minify src/index.ts",
"env:generate": "tsx src/scripts/generateEnv.ts",
"env:verify": "tsx src/scripts/verifyEnv.ts",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"test": "vitest", "test": "vitest",
"coverage": "vitest run --coverage", "test:ui": "vitest --ui",
"coverage": "vitest --coverage",
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@consumet/extensions": "github:consumet/consumet.ts#3dd0ccb", "@consumet/extensions": "github:consumet/consumet.ts#3dd0ccb",
"@haverstack/axios-fetch-adapter": "^0.12.0",
"@hono/swagger-ui": "^0.5.1", "@hono/swagger-ui": "^0.5.1",
"@hono/zod-openapi": "^0.19.5", "@hono/zod-openapi": "^0.19.5",
"@hono/zod-validator": "^0.2.2", "@hono/zod-validator": "^0.2.2",
@@ -39,16 +37,16 @@
"devDependencies": { "devDependencies": {
"@0no-co/graphqlsp": "^1.12.16", "@0no-co/graphqlsp": "^1.12.16",
"@cloudflare/vitest-pool-workers": "^0.10.7", "@cloudflare/vitest-pool-workers": "^0.10.7",
"@cloudflare/workers-types": "^4.20250423.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/lodash.mapkeys": "^4.6.9", "@types/lodash.mapkeys": "^4.6.9",
"@types/luxon": "^3.6.2", "@types/luxon": "^3.6.2",
"@types/node": "^22.10.1", "@types/node": "^22.10.1",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-istanbul": "3.2.4",
"@vitest/runner": "^3.2.4", "@vitest/runner": "^3.2.4",
"@vitest/snapshot": "^3.2.4", "@vitest/snapshot": "^3.2.4",
"@vitest/ui": "^3.2.4",
"cloudflare": "^5.2.0", "cloudflare": "^5.2.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"drizzle-kit": "^0.31.7", "drizzle-kit": "^0.31.7",

474
pnpm-lock.yaml generated
View File

@@ -15,9 +15,6 @@ importers:
"@consumet/extensions": "@consumet/extensions":
specifier: github:consumet/consumet.ts#3dd0ccb specifier: github:consumet/consumet.ts#3dd0ccb
version: https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb version: https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb
"@haverstack/axios-fetch-adapter":
specifier: ^0.12.0
version: 0.12.0(axios@0.27.2)
"@hono/swagger-ui": "@hono/swagger-ui":
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.2(hono@4.10.4) version: 0.5.2(hono@4.10.4)
@@ -32,7 +29,7 @@ importers:
version: 2.0.5(patch_hash=814a3f2d2a39f286e4f86929789e0ada33593d88cf2fb1eb3cf2cc2425c7dfaf) version: 2.0.5(patch_hash=814a3f2d2a39f286e4f86929789e0ada33593d88cf2fb1eb3cf2cc2425c7dfaf)
drizzle-orm: drizzle-orm:
specifier: ^0.44.7 specifier: ^0.44.7
version: 0.44.7(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2)) version: 0.44.7(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2))
gql.tada: gql.tada:
specifier: ^1.8.10 specifier: ^1.8.10
version: 1.8.13(graphql@16.12.0)(typescript@5.9.3) version: 1.8.13(graphql@16.12.0)(typescript@5.9.3)
@@ -75,10 +72,7 @@ importers:
version: 1.15.0(graphql@16.12.0)(typescript@5.9.3) version: 1.15.0(graphql@16.12.0)(typescript@5.9.3)
"@cloudflare/vitest-pool-workers": "@cloudflare/vitest-pool-workers":
specifier: ^0.10.7 specifier: ^0.10.7
version: 0.10.7(@cloudflare/workers-types@4.20251014.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) version: 0.10.7(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4)
"@cloudflare/workers-types":
specifier: ^4.20250423.0
version: 4.20251014.0
"@trivago/prettier-plugin-sort-imports": "@trivago/prettier-plugin-sort-imports":
specifier: ^4.3.0 specifier: ^4.3.0
version: 4.3.0(prettier@3.6.2) version: 4.3.0(prettier@3.6.2)
@@ -97,15 +91,18 @@ importers:
"@types/pngjs": "@types/pngjs":
specifier: ^6.0.5 specifier: ^6.0.5
version: 6.0.5 version: 6.0.5
"@vitest/coverage-v8": "@vitest/coverage-istanbul":
specifier: ^3.2.4 specifier: 3.2.4
version: 3.2.4(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) version: 3.2.4(vitest@3.2.4)
"@vitest/runner": "@vitest/runner":
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4 version: 3.2.4
"@vitest/snapshot": "@vitest/snapshot":
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4 version: 3.2.4
"@vitest/ui":
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4)
cloudflare: cloudflare:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0 version: 5.2.0
@@ -153,10 +150,10 @@ importers:
version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
vitest: vitest:
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) version: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
wrangler: wrangler:
specifier: ^4.51.0 specifier: ^4.51.0
version: 4.51.0(@cloudflare/workers-types@4.20251014.0) version: 4.51.0
zx: zx:
specifier: 8.1.5 specifier: 8.1.5
version: 8.1.5 version: 8.1.5
@@ -182,13 +179,6 @@ packages:
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 graphql: ^15.5.0 || ^16.0.0 || ^17.0.0
typescript: ^5.0.0 typescript: ^5.0.0
"@ampproject/remapping@2.3.0":
resolution:
{
integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==,
}
engines: { node: ">=6.0.0" }
"@asteasolutions/zod-to-openapi@7.3.4": "@asteasolutions/zod-to-openapi@7.3.4":
resolution: resolution:
{ {
@@ -204,6 +194,20 @@ packages:
} }
engines: { node: ">=6.9.0" } engines: { node: ">=6.9.0" }
"@babel/compat-data@7.28.5":
resolution:
{
integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==,
}
engines: { node: ">=6.9.0" }
"@babel/core@7.28.5":
resolution:
{
integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==,
}
engines: { node: ">=6.9.0" }
"@babel/generator@7.17.7": "@babel/generator@7.17.7":
resolution: resolution:
{ {
@@ -218,6 +222,13 @@ packages:
} }
engines: { node: ">=6.9.0" } engines: { node: ">=6.9.0" }
"@babel/helper-compilation-targets@7.27.2":
resolution:
{
integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==,
}
engines: { node: ">=6.9.0" }
"@babel/helper-environment-visitor@7.24.7": "@babel/helper-environment-visitor@7.24.7":
resolution: resolution:
{ {
@@ -232,6 +243,13 @@ packages:
} }
engines: { node: ">=6.9.0" } engines: { node: ">=6.9.0" }
"@babel/helper-globals@7.28.0":
resolution:
{
integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==,
}
engines: { node: ">=6.9.0" }
"@babel/helper-hoist-variables@7.24.7": "@babel/helper-hoist-variables@7.24.7":
resolution: resolution:
{ {
@@ -239,6 +257,22 @@ packages:
} }
engines: { node: ">=6.9.0" } engines: { node: ">=6.9.0" }
"@babel/helper-module-imports@7.27.1":
resolution:
{
integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==,
}
engines: { node: ">=6.9.0" }
"@babel/helper-module-transforms@7.28.3":
resolution:
{
integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==,
}
engines: { node: ">=6.9.0" }
peerDependencies:
"@babel/core": ^7.0.0
"@babel/helper-split-export-declaration@7.24.7": "@babel/helper-split-export-declaration@7.24.7":
resolution: resolution:
{ {
@@ -260,6 +294,20 @@ packages:
} }
engines: { node: ">=6.9.0" } engines: { node: ">=6.9.0" }
"@babel/helper-validator-option@7.27.1":
resolution:
{
integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==,
}
engines: { node: ">=6.9.0" }
"@babel/helpers@7.28.4":
resolution:
{
integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==,
}
engines: { node: ">=6.9.0" }
"@babel/parser@7.28.5": "@babel/parser@7.28.5":
resolution: resolution:
{ {
@@ -282,6 +330,13 @@ packages:
} }
engines: { node: ">=6.9.0" } engines: { node: ">=6.9.0" }
"@babel/traverse@7.28.5":
resolution:
{
integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==,
}
engines: { node: ">=6.9.0" }
"@babel/types@7.17.0": "@babel/types@7.17.0":
resolution: resolution:
{ {
@@ -296,13 +351,6 @@ packages:
} }
engines: { node: ">=6.9.0" } engines: { node: ">=6.9.0" }
"@bcoe/v8-coverage@1.0.2":
resolution:
{
integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==,
}
engines: { node: ">=18" }
"@cloudflare/kv-asset-handler@0.4.0": "@cloudflare/kv-asset-handler@0.4.0":
resolution: resolution:
{ {
@@ -441,12 +489,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
"@cloudflare/workers-types@4.20251014.0":
resolution:
{
integrity: sha512-tEW98J/kOa0TdylIUOrLKRdwkUw0rvvYVlo+Ce0mqRH3c8kSoxLzUH9gfCvwLe0M89z1RkzFovSKAW2Nwtyn3w==,
}
"@consumet/extensions@https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb": "@consumet/extensions@https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb":
resolution: resolution:
{ {
@@ -1496,14 +1538,6 @@ packages:
} }
engines: { node: ">=18.0.0" } engines: { node: ">=18.0.0" }
"@haverstack/axios-fetch-adapter@0.12.0":
resolution:
{
integrity: sha512-+9WzqzeIvEC6Qrs6ImSqaX5P+eCrWbhzR+GoB7+p8/yqxmq59CPEo0uFuu0wINU9DQuJMzyER9WYdpYVwZV9rw==,
}
peerDependencies:
axios: ^0.21.1
"@hono/swagger-ui@0.5.2": "@hono/swagger-ui@0.5.2":
resolution: resolution:
{ {
@@ -1722,6 +1756,12 @@ packages:
integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==,
} }
"@jridgewell/remapping@2.3.5":
resolution:
{
integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==,
}
"@jridgewell/resolve-uri@3.1.2": "@jridgewell/resolve-uri@3.1.2":
resolution: resolution:
{ {
@@ -1890,6 +1930,12 @@ packages:
} }
engines: { node: ">=14" } engines: { node: ">=14" }
"@polka/url@1.0.0-next.29":
resolution:
{
integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==,
}
"@poppinss/colors@4.1.5": "@poppinss/colors@4.1.5":
resolution: resolution:
{ {
@@ -2223,17 +2269,13 @@ packages:
integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==, integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==,
} }
"@vitest/coverage-v8@3.2.4": "@vitest/coverage-istanbul@3.2.4":
resolution: resolution:
{ {
integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==, integrity: sha512-IDlpuFJiWU9rhcKLkpzj8mFu/lpe64gVgnV15ZOrYx1iFzxxrxCzbExiUEKtwwXRvEiEMUS6iZeYgnMxgbqbxQ==,
} }
peerDependencies: peerDependencies:
"@vitest/browser": 3.2.4
vitest: 3.2.4 vitest: 3.2.4
peerDependenciesMeta:
"@vitest/browser":
optional: true
"@vitest/expect@3.2.4": "@vitest/expect@3.2.4":
resolution: resolution:
@@ -2279,6 +2321,14 @@ packages:
integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==, integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==,
} }
"@vitest/ui@3.2.4":
resolution:
{
integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==,
}
peerDependencies:
vitest: 3.2.4
"@vitest/utils@3.2.4": "@vitest/utils@3.2.4":
resolution: resolution:
{ {
@@ -2419,12 +2469,6 @@ packages:
} }
engines: { node: ">=12" } engines: { node: ">=12" }
ast-v8-to-istanbul@0.3.8:
resolution:
{
integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==,
}
asynckit@0.4.0: asynckit@0.4.0:
resolution: resolution:
{ {
@@ -2450,6 +2494,13 @@ packages:
integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==,
} }
baseline-browser-mapping@2.9.4:
resolution:
{
integrity: sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==,
}
hasBin: true
birpc@0.2.14: birpc@0.2.14:
resolution: resolution:
{ {
@@ -2487,6 +2538,14 @@ packages:
} }
engines: { node: ">=8" } engines: { node: ">=8" }
browserslist@4.28.1:
resolution:
{
integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==,
}
engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 }
hasBin: true
buffer-equal-constant-time@1.0.1: buffer-equal-constant-time@1.0.1:
resolution: resolution:
{ {
@@ -2535,6 +2594,12 @@ packages:
} }
engines: { node: ">= 0.4" } engines: { node: ">= 0.4" }
caniuse-lite@1.0.30001759:
resolution:
{
integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==,
}
chai@5.3.3: chai@5.3.3:
resolution: resolution:
{ {
@@ -2653,6 +2718,12 @@ packages:
integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==,
} }
convert-source-map@2.0.0:
resolution:
{
integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==,
}
cookie@1.0.2: cookie@1.0.2:
resolution: resolution:
{ {
@@ -2912,6 +2983,12 @@ packages:
integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==,
} }
electron-to-chromium@1.5.266:
resolution:
{
integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==,
}
emoji-regex@10.6.0: emoji-regex@10.6.0:
resolution: resolution:
{ {
@@ -3037,6 +3114,13 @@ packages:
engines: { node: ">=18" } engines: { node: ">=18" }
hasBin: true hasBin: true
escalade@3.2.0:
resolution:
{
integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==,
}
engines: { node: ">=6" }
estree-walker@3.0.3: estree-walker@3.0.3:
resolution: resolution:
{ {
@@ -3115,6 +3199,12 @@ packages:
} }
engines: { node: ^12.20 || >= 14.13 } engines: { node: ^12.20 || >= 14.13 }
fflate@0.8.2:
resolution:
{
integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==,
}
fill-range@7.1.1: fill-range@7.1.1:
resolution: resolution:
{ {
@@ -3122,6 +3212,12 @@ packages:
} }
engines: { node: ">=8" } engines: { node: ">=8" }
flatted@3.3.3:
resolution:
{
integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==,
}
follow-redirects@1.15.11: follow-redirects@1.15.11:
resolution: resolution:
{ {
@@ -3203,6 +3299,13 @@ packages:
} }
engines: { node: ">= 0.4" } engines: { node: ">= 0.4" }
gensync@1.0.0-beta.2:
resolution:
{
integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==,
}
engines: { node: ">=6.9.0" }
get-east-asian-width@1.4.0: get-east-asian-width@1.4.0:
resolution: resolution:
{ {
@@ -3521,6 +3624,13 @@ packages:
} }
engines: { node: ">=8" } engines: { node: ">=8" }
istanbul-lib-instrument@6.0.3:
resolution:
{
integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==,
}
engines: { node: ">=10" }
istanbul-lib-report@3.0.1: istanbul-lib-report@3.0.1:
resolution: resolution:
{ {
@@ -3600,6 +3710,14 @@ packages:
engines: { node: ">=6" } engines: { node: ">=6" }
hasBin: true hasBin: true
json5@2.2.3:
resolution:
{
integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==,
}
engines: { node: ">=6" }
hasBin: true
jwa@2.0.1: jwa@2.0.1:
resolution: resolution:
{ {
@@ -3687,6 +3805,12 @@ packages:
integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==,
} }
lru-cache@5.1.1:
resolution:
{
integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==,
}
luxon@3.7.2: luxon@3.7.2:
resolution: resolution:
{ {
@@ -3814,6 +3938,13 @@ packages:
engines: { node: ">=10" } engines: { node: ">=10" }
hasBin: true hasBin: true
mrmime@2.0.1:
resolution:
{
integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==,
}
engines: { node: ">=10" }
ms@2.1.3: ms@2.1.3:
resolution: resolution:
{ {
@@ -3855,6 +3986,12 @@ packages:
} }
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
node-releases@2.0.27:
resolution:
{
integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==,
}
npm-run-path@5.3.0: npm-run-path@5.3.0:
resolution: resolution:
{ {
@@ -4095,6 +4232,13 @@ packages:
integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==,
} }
semver@6.3.1:
resolution:
{
integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==,
}
hasBin: true
semver@7.7.3: semver@7.7.3:
resolution: resolution:
{ {
@@ -4150,6 +4294,13 @@ packages:
integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==, integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==,
} }
sirv@3.0.2:
resolution:
{
integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==,
}
engines: { node: ">=18" }
slice-ansi@5.0.0: slice-ansi@5.0.0:
resolution: resolution:
{ {
@@ -4348,6 +4499,13 @@ packages:
} }
engines: { node: ">=8.0" } engines: { node: ">=8.0" }
totalist@3.0.1:
resolution:
{
integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==,
}
engines: { node: ">=6" }
tr46@0.0.3: tr46@0.0.3:
resolution: resolution:
{ {
@@ -4427,6 +4585,15 @@ packages:
integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==,
} }
update-browserslist-db@1.2.2:
resolution:
{
integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==,
}
hasBin: true
peerDependencies:
browserslist: ">= 4.21.0"
urlpattern-polyfill@10.1.0: urlpattern-polyfill@10.1.0:
resolution: resolution:
{ {
@@ -4696,6 +4863,12 @@ packages:
utf-8-validate: utf-8-validate:
optional: true optional: true
yallist@3.1.1:
resolution:
{
integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==,
}
yaml@2.8.1: yaml@2.8.1:
resolution: resolution:
{ {
@@ -4747,11 +4920,6 @@ snapshots:
graphql: 16.12.0 graphql: 16.12.0
typescript: 5.9.3 typescript: 5.9.3
"@ampproject/remapping@2.3.0":
dependencies:
"@jridgewell/gen-mapping": 0.3.13
"@jridgewell/trace-mapping": 0.3.31
"@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.76)": "@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.76)":
dependencies: dependencies:
openapi3-ts: 4.5.0 openapi3-ts: 4.5.0
@@ -4763,6 +4931,28 @@ snapshots:
js-tokens: 4.0.0 js-tokens: 4.0.0
picocolors: 1.1.1 picocolors: 1.1.1
"@babel/compat-data@7.28.5": {}
"@babel/core@7.28.5":
dependencies:
"@babel/code-frame": 7.27.1
"@babel/generator": 7.28.5
"@babel/helper-compilation-targets": 7.27.2
"@babel/helper-module-transforms": 7.28.3(@babel/core@7.28.5)
"@babel/helpers": 7.28.4
"@babel/parser": 7.28.5
"@babel/template": 7.27.2
"@babel/traverse": 7.28.5
"@babel/types": 7.28.5
"@jridgewell/remapping": 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
"@babel/generator@7.17.7": "@babel/generator@7.17.7":
dependencies: dependencies:
"@babel/types": 7.17.0 "@babel/types": 7.17.0
@@ -4777,6 +4967,14 @@ snapshots:
"@jridgewell/trace-mapping": 0.3.31 "@jridgewell/trace-mapping": 0.3.31
jsesc: 3.1.0 jsesc: 3.1.0
"@babel/helper-compilation-targets@7.27.2":
dependencies:
"@babel/compat-data": 7.28.5
"@babel/helper-validator-option": 7.27.1
browserslist: 4.28.1
lru-cache: 5.1.1
semver: 6.3.1
"@babel/helper-environment-visitor@7.24.7": "@babel/helper-environment-visitor@7.24.7":
dependencies: dependencies:
"@babel/types": 7.28.5 "@babel/types": 7.28.5
@@ -4786,10 +4984,28 @@ snapshots:
"@babel/template": 7.27.2 "@babel/template": 7.27.2
"@babel/types": 7.28.5 "@babel/types": 7.28.5
"@babel/helper-globals@7.28.0": {}
"@babel/helper-hoist-variables@7.24.7": "@babel/helper-hoist-variables@7.24.7":
dependencies: dependencies:
"@babel/types": 7.28.5 "@babel/types": 7.28.5
"@babel/helper-module-imports@7.27.1":
dependencies:
"@babel/traverse": 7.28.5
"@babel/types": 7.28.5
transitivePeerDependencies:
- supports-color
"@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)":
dependencies:
"@babel/core": 7.28.5
"@babel/helper-module-imports": 7.27.1
"@babel/helper-validator-identifier": 7.28.5
"@babel/traverse": 7.28.5
transitivePeerDependencies:
- supports-color
"@babel/helper-split-export-declaration@7.24.7": "@babel/helper-split-export-declaration@7.24.7":
dependencies: dependencies:
"@babel/types": 7.28.5 "@babel/types": 7.28.5
@@ -4798,6 +5014,13 @@ snapshots:
"@babel/helper-validator-identifier@7.28.5": {} "@babel/helper-validator-identifier@7.28.5": {}
"@babel/helper-validator-option@7.27.1": {}
"@babel/helpers@7.28.4":
dependencies:
"@babel/template": 7.27.2
"@babel/types": 7.28.5
"@babel/parser@7.28.5": "@babel/parser@7.28.5":
dependencies: dependencies:
"@babel/types": 7.28.5 "@babel/types": 7.28.5
@@ -4823,6 +5046,18 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
"@babel/traverse@7.28.5":
dependencies:
"@babel/code-frame": 7.27.1
"@babel/generator": 7.28.5
"@babel/helper-globals": 7.28.0
"@babel/parser": 7.28.5
"@babel/template": 7.27.2
"@babel/types": 7.28.5
debug: 4.4.3
transitivePeerDependencies:
- supports-color
"@babel/types@7.17.0": "@babel/types@7.17.0":
dependencies: dependencies:
"@babel/helper-validator-identifier": 7.28.5 "@babel/helper-validator-identifier": 7.28.5
@@ -4833,8 +5068,6 @@ snapshots:
"@babel/helper-string-parser": 7.27.1 "@babel/helper-string-parser": 7.27.1
"@babel/helper-validator-identifier": 7.28.5 "@babel/helper-validator-identifier": 7.28.5
"@bcoe/v8-coverage@1.0.2": {}
"@cloudflare/kv-asset-handler@0.4.0": "@cloudflare/kv-asset-handler@0.4.0":
dependencies: dependencies:
mime: 3.0.0 mime: 3.0.0
@@ -4855,7 +5088,7 @@ snapshots:
optionalDependencies: optionalDependencies:
workerd: 1.20251125.0 workerd: 1.20251125.0
"@cloudflare/vitest-pool-workers@0.10.7(@cloudflare/workers-types@4.20251014.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))": "@cloudflare/vitest-pool-workers@0.10.7(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4)":
dependencies: dependencies:
"@vitest/runner": 3.2.4 "@vitest/runner": 3.2.4
"@vitest/snapshot": 3.2.4 "@vitest/snapshot": 3.2.4
@@ -4864,8 +5097,8 @@ snapshots:
devalue: 5.5.0 devalue: 5.5.0
miniflare: 4.20251109.1 miniflare: 4.20251109.1
semver: 7.7.3 semver: 7.7.3
vitest: 3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) vitest: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
wrangler: 4.48.0(@cloudflare/workers-types@4.20251014.0) wrangler: 4.48.0
zod: 3.25.76 zod: 3.25.76
transitivePeerDependencies: transitivePeerDependencies:
- "@cloudflare/workers-types" - "@cloudflare/workers-types"
@@ -4902,8 +5135,6 @@ snapshots:
"@cloudflare/workerd-windows-64@1.20251125.0": "@cloudflare/workerd-windows-64@1.20251125.0":
optional: true optional: true
"@cloudflare/workers-types@4.20251014.0": {}
"@consumet/extensions@https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb": "@consumet/extensions@https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb":
dependencies: dependencies:
ascii-url-encoder: 1.2.0 ascii-url-encoder: 1.2.0
@@ -5316,10 +5547,6 @@ snapshots:
"@repeaterjs/repeater": 3.0.6 "@repeaterjs/repeater": 3.0.6
tslib: 2.8.1 tslib: 2.8.1
"@haverstack/axios-fetch-adapter@0.12.0(axios@0.27.2)":
dependencies:
axios: 0.27.2
"@hono/swagger-ui@0.5.2(hono@4.10.4)": "@hono/swagger-ui@0.5.2(hono@4.10.4)":
dependencies: dependencies:
hono: 4.10.4 hono: 4.10.4
@@ -5433,6 +5660,11 @@ snapshots:
"@jridgewell/sourcemap-codec": 1.5.5 "@jridgewell/sourcemap-codec": 1.5.5
"@jridgewell/trace-mapping": 0.3.31 "@jridgewell/trace-mapping": 0.3.31
"@jridgewell/remapping@2.3.5":
dependencies:
"@jridgewell/gen-mapping": 0.3.13
"@jridgewell/trace-mapping": 0.3.31
"@jridgewell/resolve-uri@3.1.2": {} "@jridgewell/resolve-uri@3.1.2": {}
"@jridgewell/source-map@0.3.11": "@jridgewell/source-map@0.3.11":
@@ -5538,6 +5770,8 @@ snapshots:
"@pkgjs/parseargs@0.11.0": "@pkgjs/parseargs@0.11.0":
optional: true optional: true
"@polka/url@1.0.0-next.29": {}
"@poppinss/colors@4.1.5": "@poppinss/colors@4.1.5":
dependencies: dependencies:
kleur: 4.1.5 kleur: 4.1.5
@@ -5706,22 +5940,19 @@ snapshots:
"@types/node": 22.18.13 "@types/node": 22.18.13
optional: true optional: true
"@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))": "@vitest/coverage-istanbul@3.2.4(vitest@3.2.4)":
dependencies: dependencies:
"@ampproject/remapping": 2.3.0 "@istanbuljs/schema": 0.1.3
"@bcoe/v8-coverage": 1.0.2
ast-v8-to-istanbul: 0.3.8
debug: 4.4.3 debug: 4.4.3
istanbul-lib-coverage: 3.2.2 istanbul-lib-coverage: 3.2.2
istanbul-lib-instrument: 6.0.3
istanbul-lib-report: 3.0.1 istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6 istanbul-lib-source-maps: 5.0.6
istanbul-reports: 3.2.0 istanbul-reports: 3.2.0
magic-string: 0.30.21
magicast: 0.3.5 magicast: 0.3.5
std-env: 3.10.0
test-exclude: 7.0.1 test-exclude: 7.0.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vitest: 3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) vitest: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -5761,6 +5992,17 @@ snapshots:
dependencies: dependencies:
tinyspy: 4.0.4 tinyspy: 4.0.4
"@vitest/ui@3.2.4(vitest@3.2.4)":
dependencies:
"@vitest/utils": 3.2.4
fflate: 0.8.2
flatted: 3.3.3
pathe: 2.0.3
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
"@vitest/utils@3.2.4": "@vitest/utils@3.2.4":
dependencies: dependencies:
"@vitest/pretty-format": 3.2.4 "@vitest/pretty-format": 3.2.4
@@ -5835,12 +6077,6 @@ snapshots:
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
ast-v8-to-istanbul@0.3.8:
dependencies:
"@jridgewell/trace-mapping": 0.3.31
estree-walker: 3.0.3
js-tokens: 9.0.1
asynckit@0.4.0: {} asynckit@0.4.0: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
@@ -5856,6 +6092,8 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
baseline-browser-mapping@2.9.4: {}
birpc@0.2.14: {} birpc@0.2.14: {}
blake3-wasm@2.1.5: {} blake3-wasm@2.1.5: {}
@@ -5873,6 +6111,14 @@ snapshots:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.9.4
caniuse-lite: 1.0.30001759
electron-to-chromium: 1.5.266
node-releases: 2.0.27
update-browserslist-db: 1.2.2(browserslist@4.28.1)
buffer-equal-constant-time@1.0.1: {} buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
@@ -5902,6 +6148,8 @@ snapshots:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
caniuse-lite@1.0.30001759: {}
chai@5.3.3: chai@5.3.3:
dependencies: dependencies:
assertion-error: 2.0.1 assertion-error: 2.0.1
@@ -5989,6 +6237,8 @@ snapshots:
commander@2.20.3: commander@2.20.3:
optional: true optional: true
convert-source-map@2.0.0: {}
cookie@1.0.2: {} cookie@1.0.2: {}
cross-inspect@1.0.1: cross-inspect@1.0.1:
@@ -6069,9 +6319,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2)): drizzle-orm@0.44.7(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2)):
optionalDependencies: optionalDependencies:
"@cloudflare/workers-types": 4.20251014.0
"@libsql/client": 0.15.4 "@libsql/client": 0.15.4
bun-types: 1.3.1(@types/react@19.2.2) bun-types: 1.3.1(@types/react@19.2.2)
@@ -6087,6 +6336,8 @@ snapshots:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
electron-to-chromium@1.5.266: {}
emoji-regex@10.6.0: {} emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
@@ -6241,6 +6492,8 @@ snapshots:
"@esbuild/win32-ia32": 0.27.0 "@esbuild/win32-ia32": 0.27.0
"@esbuild/win32-x64": 0.27.0 "@esbuild/win32-x64": 0.27.0
escalade@3.2.0: {}
estree-walker@3.0.3: estree-walker@3.0.3:
dependencies: dependencies:
"@types/estree": 1.0.8 "@types/estree": 1.0.8
@@ -6289,10 +6542,14 @@ snapshots:
web-streams-polyfill: 3.3.3 web-streams-polyfill: 3.3.3
optional: true optional: true
fflate@0.8.2: {}
fill-range@7.1.1: fill-range@7.1.1:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
flatted@3.3.3: {}
follow-redirects@1.15.11: {} follow-redirects@1.15.11: {}
for-each@0.3.5: for-each@0.3.5:
@@ -6342,6 +6599,8 @@ snapshots:
generator-function@2.0.1: {} generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {}
get-east-asian-width@1.4.0: {} get-east-asian-width@1.4.0: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
@@ -6532,6 +6791,16 @@ snapshots:
istanbul-lib-coverage@3.2.2: {} istanbul-lib-coverage@3.2.2: {}
istanbul-lib-instrument@6.0.3:
dependencies:
"@babel/core": 7.28.5
"@babel/parser": 7.28.5
"@istanbuljs/schema": 0.1.3
istanbul-lib-coverage: 3.2.2
semver: 7.7.3
transitivePeerDependencies:
- supports-color
istanbul-lib-report@3.0.1: istanbul-lib-report@3.0.1:
dependencies: dependencies:
istanbul-lib-coverage: 3.2.2 istanbul-lib-coverage: 3.2.2
@@ -6574,6 +6843,8 @@ snapshots:
jsesc@3.1.0: {} jsesc@3.1.0: {}
json5@2.2.3: {}
jwa@2.0.1: jwa@2.0.1:
dependencies: dependencies:
buffer-equal-constant-time: 1.0.1 buffer-equal-constant-time: 1.0.1
@@ -6647,6 +6918,10 @@ snapshots:
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
luxon@3.7.2: {} luxon@3.7.2: {}
magic-string@0.30.21: magic-string@0.30.21:
@@ -6730,6 +7005,8 @@ snapshots:
mkdirp@3.0.1: {} mkdirp@3.0.1: {}
mrmime@2.0.1: {}
ms@2.1.3: {} ms@2.1.3: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
@@ -6747,6 +7024,8 @@ snapshots:
formdata-polyfill: 4.0.10 formdata-polyfill: 4.0.10
optional: true optional: true
node-releases@2.0.27: {}
npm-run-path@5.3.0: npm-run-path@5.3.0:
dependencies: dependencies:
path-key: 4.0.0 path-key: 4.0.0
@@ -6882,6 +7161,8 @@ snapshots:
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
semver@6.3.1: {}
semver@7.7.3: {} semver@7.7.3: {}
set-function-length@1.2.2: set-function-length@1.2.2:
@@ -6933,6 +7214,12 @@ snapshots:
dependencies: dependencies:
is-arrayish: 0.3.4 is-arrayish: 0.3.4
sirv@3.0.2:
dependencies:
"@polka/url": 1.0.0-next.29
mrmime: 2.0.1
totalist: 3.0.1
slice-ansi@5.0.0: slice-ansi@5.0.0:
dependencies: dependencies:
ansi-styles: 6.2.3 ansi-styles: 6.2.3
@@ -7035,6 +7322,8 @@ snapshots:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
totalist@3.0.1: {}
tr46@0.0.3: {} tr46@0.0.3: {}
ts-morph@22.0.0: ts-morph@22.0.0:
@@ -7069,6 +7358,12 @@ snapshots:
dependencies: dependencies:
pathe: 2.0.3 pathe: 2.0.3
update-browserslist-db@1.2.2(browserslist@4.28.1):
dependencies:
browserslist: 4.28.1
escalade: 3.2.0
picocolors: 1.1.1
urlpattern-polyfill@10.1.0: {} urlpattern-polyfill@10.1.0: {}
util@0.12.5: util@0.12.5:
@@ -7128,7 +7423,7 @@ snapshots:
tsx: 4.20.6 tsx: 4.20.6
yaml: 2.8.1 yaml: 2.8.1
vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): vitest@3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1):
dependencies: dependencies:
"@types/chai": 5.2.3 "@types/chai": 5.2.3
"@vitest/expect": 3.2.4 "@vitest/expect": 3.2.4
@@ -7155,6 +7450,7 @@ snapshots:
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
"@types/node": 22.18.13 "@types/node": 22.18.13
"@vitest/ui": 3.2.4(vitest@3.2.4)
transitivePeerDependencies: transitivePeerDependencies:
- jiti - jiti
- less - less
@@ -7222,7 +7518,7 @@ snapshots:
"@cloudflare/workerd-linux-arm64": 1.20251125.0 "@cloudflare/workerd-linux-arm64": 1.20251125.0
"@cloudflare/workerd-windows-64": 1.20251125.0 "@cloudflare/workerd-windows-64": 1.20251125.0
wrangler@4.48.0(@cloudflare/workers-types@4.20251014.0): wrangler@4.48.0:
dependencies: dependencies:
"@cloudflare/kv-asset-handler": 0.4.0 "@cloudflare/kv-asset-handler": 0.4.0
"@cloudflare/unenv-preset": 2.7.10(unenv@2.0.0-rc.24)(workerd@1.20251109.0) "@cloudflare/unenv-preset": 2.7.10(unenv@2.0.0-rc.24)(workerd@1.20251109.0)
@@ -7233,13 +7529,12 @@ snapshots:
unenv: 2.0.0-rc.24 unenv: 2.0.0-rc.24
workerd: 1.20251109.0 workerd: 1.20251109.0
optionalDependencies: optionalDependencies:
"@cloudflare/workers-types": 4.20251014.0
fsevents: 2.3.3 fsevents: 2.3.3
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
wrangler@4.51.0(@cloudflare/workers-types@4.20251014.0): wrangler@4.51.0:
dependencies: dependencies:
"@cloudflare/kv-asset-handler": 0.4.1 "@cloudflare/kv-asset-handler": 0.4.1
"@cloudflare/unenv-preset": 2.7.11(unenv@2.0.0-rc.24)(workerd@1.20251125.0) "@cloudflare/unenv-preset": 2.7.11(unenv@2.0.0-rc.24)(workerd@1.20251125.0)
@@ -7250,7 +7545,6 @@ snapshots:
unenv: 2.0.0-rc.24 unenv: 2.0.0-rc.24
workerd: 1.20251125.0 workerd: 1.20251125.0
optionalDependencies: optionalDependencies:
"@cloudflare/workers-types": 4.20251014.0
fsevents: 2.3.3 fsevents: 2.3.3
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
@@ -7279,6 +7573,8 @@ snapshots:
ws@8.18.3: ws@8.18.3:
optional: true optional: true
yallist@3.1.1: {}
yaml@2.8.1: {} yaml@2.8.1: {}
youch-core@0.3.3: youch-core@0.3.3:

View File

@@ -1,5 +0,0 @@
import { ANIME, META } from "@consumet/extensions";
import fetchAdapter from "@haverstack/axios-fetch-adapter";
const gogoAnime = new ANIME.Gogoanime(undefined, undefined, fetchAdapter);
export const aniList = new META.Anilist(gogoAnime, undefined, fetchAdapter);

View File

@@ -1,30 +0,0 @@
import type { HonoRequest } from "hono";
export function getCurrentDomain(req: HonoRequest): string | undefined;
export function getCurrentDomain(
req: HonoRequest,
avoidLocalhost: false,
): string;
export function getCurrentDomain(
req: HonoRequest,
avoidLocalhost: true,
): string | undefined;
export function getCurrentDomain(req: HonoRequest, avoidLocalhost = true) {
let domain = req.url.replace(req.path, "");
if (domain.includes("?")) {
domain = domain.split("?")[0];
}
if (avoidLocalhost) {
if (
domain.includes("localhost") ||
domain.includes("127.0.0.1") ||
domain.includes("192.168.1")
) {
console.log("Domain is localhost, returning undefined");
return;
}
}
return domain;
}

View File

@@ -1,23 +0,0 @@
export async function logStep<T = void>(
inProgressText: string,
step: () => Promise<T> | T,
): Promise<T>;
export async function logStep<T = void>(
inProgressText: string,
step: () => Promise<T> | T,
doneText: string,
): Promise<T>;
export async function logStep<T = void>(
inProgressText: string,
step: () => Promise<T> | T,
doneText: string = `Completed step "${inProgressText}"`,
) {
console.time(doneText);
console.log(`${inProgressText}...`);
return Promise.resolve(step()).then((value) => {
console.timeEnd(doneText);
return value;
});
}

View File

@@ -30,6 +30,6 @@ describe("readEnvVariable", () => {
}); });
it("env not defined, returns default value", () => { it("env not defined, returns default value", () => {
expect(readEnvVariable<boolean>("ENABLE_ANIFY", undefined)).toBe(true); expect(readEnvVariable<boolean>("ENABLE_ANIFY", {})).toBe(true);
}); });
}); });

View File

@@ -1,15 +1,9 @@
// import { createClient } from "@libsql/client";
import { env as cloudflareEnv } from "cloudflare:workers"; import { env as cloudflareEnv } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1"; import { drizzle } from "drizzle-orm/d1";
type Db = ReturnType<typeof drizzle>; type Db = ReturnType<typeof drizzle>;
// let db: Db | null = null;
export function getDb(env: Cloudflare.Env = cloudflareEnv): Db { export function getDb(env: Cloudflare.Env = cloudflareEnv): Db {
// if (db) {
// return db;
// }
const db = drizzle(env.DB, { logger: true }); const db = drizzle(env.DB, { logger: true });
return db; return db;
} }

View File

@@ -0,0 +1,109 @@
import { GraphQLError } from "graphql";
import { describe, expect, it, vi } from "vitest";
import { markEpisodeAsWatched } from "~/services/episodes/markEpisodeAsWatched/anilist";
import { markEpisodeAsWatchedMutation } from "./markEpisodeAsWatched";
vi.mock("~/services/episodes/markEpisodeAsWatched/anilist", () => ({
markEpisodeAsWatched: vi.fn(),
}));
vi.mock("~/services/watch-status", () => ({
updateWatchStatus: vi.fn(),
}));
describe("markEpisodeAsWatched mutation", () => {
it("should throw GraphQLError if aniListToken is missing", async () => {
await expect(
markEpisodeAsWatchedMutation(
null,
{ input: { titleId: 1, episodeNumber: 1, isComplete: false } },
{ aniListToken: undefined } as any,
),
).rejects.toThrow(
new GraphQLError(
"AniList token is required. Please provide X-AniList-Token header.",
{
extensions: { code: "UNAUTHORIZED" },
},
),
);
});
it("should call markEpisodeAsWatched service", async () => {
vi.mocked(markEpisodeAsWatched).mockResolvedValue({} as any);
await markEpisodeAsWatchedMutation(
null,
{ input: { titleId: 1, episodeNumber: 1, isComplete: false } },
{ aniListToken: "token" } as any,
);
expect(markEpisodeAsWatched).toHaveBeenCalledWith("token", 1, 1, false);
});
it("should update watch status locally if isComplete is true and deviceId is present", async () => {
vi.mocked(markEpisodeAsWatched).mockResolvedValue({} as any);
const { updateWatchStatus } = await import("~/services/watch-status");
await markEpisodeAsWatchedMutation(
null,
{ input: { titleId: 1, episodeNumber: 1, isComplete: true } },
{ aniListToken: "token", deviceId: "device-id" } as any,
);
expect(updateWatchStatus).toHaveBeenCalledWith("device-id", 1, "COMPLETED");
});
it("should not update watch status locally if deviceId is missing", async () => {
vi.mocked(markEpisodeAsWatched).mockResolvedValue({} as any);
const { updateWatchStatus } = await import("~/services/watch-status");
vi.mocked(updateWatchStatus).mockClear();
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
await markEpisodeAsWatchedMutation(
null,
{ input: { titleId: 1, episodeNumber: 1, isComplete: true } },
{ aniListToken: "token" } as any,
);
expect(updateWatchStatus).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
"Device ID not found in context, skipping watch status update",
);
});
it("should throw GraphQLError if service return null", async () => {
vi.mocked(markEpisodeAsWatched).mockResolvedValue(null as any);
await expect(
markEpisodeAsWatchedMutation(
null,
{ input: { titleId: 1, episodeNumber: 1, isComplete: false } },
{ aniListToken: "token" } as any,
),
).rejects.toThrow(
new GraphQLError("Failed to mark episode as watched", {
extensions: { code: "INTERNAL_SERVER_ERROR" },
}),
);
});
it("should catch errors and throw GraphQLError", async () => {
vi.mocked(markEpisodeAsWatched).mockRejectedValue(new Error("Foo"));
await expect(
markEpisodeAsWatchedMutation(
null,
{ input: { titleId: 1, episodeNumber: 1, isComplete: false } },
{ aniListToken: "token" } as any,
),
).rejects.toThrow(
new GraphQLError("Failed to mark episode as watched", {
extensions: { code: "INTERNAL_SERVER_ERROR" },
}),
);
});
});

View File

@@ -0,0 +1,55 @@
import { GraphQLError } from "graphql";
import { describe, expect, it, vi } from "vitest";
import { updateWatchStatus } from "~/services/watch-status";
import { updateWatchStatusMutation } from "./updateWatchStatus";
vi.mock("~/services/watch-status", () => ({
updateWatchStatus: vi.fn(),
}));
describe("updateWatchStatus mutation", () => {
it("should throw GraphQLError if deviceId is missing", async () => {
await expect(
updateWatchStatusMutation(
null,
{ input: { titleId: 1, watchStatus: "CURRENT" } },
{ deviceId: undefined } as any,
),
).rejects.toThrow(
new GraphQLError(
"Device ID is required. Please provide X-Device-ID header.",
{
extensions: { code: "BAD_REQUEST" },
},
),
);
});
it("should call updateWatchStatus service with correct parameters", async () => {
await updateWatchStatusMutation(
null,
{ input: { titleId: 1, watchStatus: "CURRENT" } },
{ deviceId: "device-id" } as any,
);
expect(updateWatchStatus).toHaveBeenCalledWith("device-id", 1, "CURRENT");
});
it("should catch service errors and throw GraphQLError", async () => {
vi.mocked(updateWatchStatus).mockRejectedValue(new Error("Service error"));
await expect(
updateWatchStatusMutation(
null,
{ input: { titleId: 1, watchStatus: "CURRENT" } },
{ deviceId: "device-id" } as any,
),
).rejects.toThrow(
new GraphQLError("Failed to update watch status", {
extensions: { code: "INTERNAL_SERVER_ERROR" },
}),
);
});
});

View File

@@ -0,0 +1,91 @@
import { env } from "cloudflare:workers";
import { GraphQLError } from "graphql";
import { describe, expect, it, vi } from "vitest";
import type { GraphQLContext } from "~/context";
import { home } from "./home";
enum HomeCategory {
WATCHING,
PLANNING,
}
describe("home resolver", () => {
const mockContext = {
user: { name: "testuser" },
aniListToken: "test-token",
} as GraphQLContext;
it("should fetch WATCHING titles using CURRENT status filter", async () => {
const mockResponse = { some: "data" };
const mockStub = {
getTitles: vi.fn().mockResolvedValue(mockResponse),
};
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
getByName: vi.fn().mockResolvedValue(mockStub),
};
const result = await home(
null,
{ category: HomeCategory.WATCHING },
mockContext,
);
expect(result).toEqual(mockResponse);
expect(env.ANILIST_DO.getByName).toHaveBeenCalledWith("GLOBAL");
expect(mockStub.getTitles).toHaveBeenCalledWith(
"testuser",
1,
["CURRENT"],
"test-token",
);
});
it("should fetch PLANNING titles using PLANNING, PAUSED, REPEATING status filters", async () => {
const mockResponse = { some: "data" };
const mockStub = {
getTitles: vi.fn().mockResolvedValue(mockResponse),
};
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
getByName: vi.fn().mockResolvedValue(mockStub),
};
const result = await home(
null,
{ category: HomeCategory.PLANNING, page: 2 },
mockContext,
);
expect(result).toEqual(mockResponse);
expect(mockStub.getTitles).toHaveBeenCalledWith(
"testuser",
2,
["PLANNING", "PAUSED", "REPEATING"],
"test-token",
);
});
it("should throw GraphQLError if Durable Object response is null", async () => {
const mockStub = {
getTitles: vi.fn().mockResolvedValue(null),
};
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
getByName: vi.fn().mockResolvedValue(mockStub),
};
await expect(
home(null, { category: HomeCategory.WATCHING }, mockContext),
).rejects.toThrow(
new GraphQLError("Failed to fetch 0 titles", {
extensions: { code: "INTERNAL_SERVER_ERROR" },
}),
);
});
});

View File

@@ -0,0 +1,51 @@
import { GraphQLError } from "graphql";
import { describe, expect, it, vi } from "vitest";
import { fetchPopularTitlesFromAnilist } from "~/services/popular/browse/anilist";
import { popularBrowse } from "./popularBrowse";
vi.mock("~/services/popular/browse/anilist", () => ({
fetchPopularTitlesFromAnilist: vi.fn(),
}));
describe("popularBrowse resolver", () => {
it("should fetch titles with default limit", async () => {
const mockResponse = {
trending: ["trending"],
popular: ["popular"],
upcoming: ["upcoming"],
};
vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(mockResponse);
const result = await popularBrowse(null, {}, {} as any);
expect(result).toEqual(mockResponse);
expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith(10);
});
it("should fetch titles with provided limit", async () => {
const mockResponse = {};
vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(mockResponse);
await popularBrowse(null, { limit: 20 }, {} as any);
expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith(20);
});
it("should throw GraphQLError if service returns null", async () => {
vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(undefined);
await expect(popularBrowse(null, {}, {} as any)).rejects.toThrow(
new GraphQLError("Failed to fetch popular titles", {
extensions: { code: "INTERNAL_SERVER_ERROR" },
}),
);
});
it("should map response correctly to trending, popular, and upcoming", async () => {
vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue({} as any);
const result = await popularBrowse(null, {}, {} as any);
expect(result).toEqual({ trending: [], popular: [], upcoming: [] });
});
});

View File

@@ -0,0 +1,59 @@
import { GraphQLError } from "graphql";
import { describe, expect, it, vi } from "vitest";
import { fetchPopularTitlesFromAnilist } from "~/services/popular/category/anilist";
import { popularByCategory } from "./popularByCategory";
vi.mock("~/services/popular/category/anilist", () => ({
fetchPopularTitlesFromAnilist: vi.fn(),
}));
describe("popularByCategory resolver", () => {
it("should fetch titles for a specific category with page and limit", async () => {
const mockResponse = { results: ["title"], hasNextPage: true };
vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(
mockResponse as any,
);
const result = await popularByCategory(
null,
{ category: "trending", page: 2, limit: 20 },
{} as any,
);
expect(result).toEqual(mockResponse);
expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith(
"trending",
2,
20,
);
});
it("should use default page and limit", async () => {
const mockResponse = { results: [], hasNextPage: false };
vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(
mockResponse as any,
);
await popularByCategory(null, { category: "popular" }, {} as any);
expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith(
"popular",
1,
10,
);
});
it("should throw GraphQLError if service returns null", async () => {
vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(undefined);
await expect(
popularByCategory(null, { category: "upcoming" }, {} as any),
).rejects.toThrow(
new GraphQLError("Failed to fetch upcoming titles", {
extensions: { code: "INTERNAL_SERVER_ERROR" },
}),
);
});
});

View File

@@ -0,0 +1,44 @@
import { GraphQLError } from "graphql";
import { describe, expect, it, vi } from "vitest";
import { getUser } from "~/services/auth/anilist/getUser";
import { user } from "./user";
vi.mock("~/services/auth/anilist/getUser", () => ({
getUser: vi.fn(),
}));
describe("user resolver", () => {
it("should throw GraphQLError (UNAUTHORIZED) if aniListToken is missing", async () => {
await expect(
user(null, {}, { aniListToken: undefined } as any),
).rejects.toThrow(
new GraphQLError("Unauthorized", {
extensions: { code: "UNAUTHORIZED" },
}),
);
});
it("should fetch user if token is present", async () => {
const mockUser = { id: 1, name: "test" };
vi.mocked(getUser).mockResolvedValue(mockUser as any);
const result = await user(null, {}, { aniListToken: "token" } as any);
expect(result).toEqual(mockUser);
expect(getUser).toHaveBeenCalledWith("token");
});
it("should throw GraphQLError if user service returns null", async () => {
vi.mocked(getUser).mockResolvedValue(null);
await expect(
user(null, {}, { aniListToken: "token" } as any),
).rejects.toThrow(
new GraphQLError("Failed to fetch user", {
extensions: { code: "INTERNAL_SERVER_ERROR" },
}),
);
});
});

View File

@@ -1,47 +0,0 @@
import { $ } from "bun";
import { Project } from "ts-morph";
import { logStep } from "~/libs/logStep";
await logStep(
'Re-generating "env.d.ts"',
() => $`bunx wrangler types src/types/env.d.ts`.quiet(),
"Generated env.d.ts",
);
const secretNames = await logStep(
"Fetching secrets from Cloudflare",
async (): Promise<string[]> => {
const { stdout } = await $`bunx wrangler secret list`.quiet();
return JSON.parse(stdout.toString()).map(
(secret: { name: string; type: "secret_text" }) => secret.name,
);
},
"Fetched secrets",
);
const project = new Project({});
const envSourceFile = project.addSourceFileAtPath("src/types/env.d.ts");
envSourceFile.insertImportDeclaration(2, {
isTypeOnly: true,
moduleSpecifier: "hono",
namedImports: ["Env as HonoEnv"],
});
envSourceFile
.getInterfaceOrThrow("Env")
.addExtends(["HonoEnv", "Record<string, unknown>"]);
envSourceFile.getInterfaceOrThrow("Env").addProperties(
secretNames.map((name) => ({
name,
type: `string`,
})),
);
await project.save();
await logStep(
"Formatting env.d.ts",
() => $`bunx prettier --write src/types/env.d.ts`.quiet(),
"Formatted env.d.ts",
);

View File

@@ -1,42 +0,0 @@
import { $, sleep, spawn } from "bun";
import { readFile } from "fs/promises";
import { logStep } from "~/libs/logStep";
await $`cp src/types/env.d.ts /tmp/env.d.ts`.quiet();
await logStep(
'Generating "env.d.ts"',
() => import("./generateEnv"),
"Generated env.d.ts",
);
await logStep("Comparing env.d.ts", async () => {
function filterComments(content: Buffer) {
return content
.toString()
.split("\n")
.filter((line) => !line.trim().startsWith("//"))
.join("\n");
}
const currentFileContent = filterComments(await readFile("/tmp/env.d.ts"));
const generatedFileContent = filterComments(
await readFile("src/types/env.d.ts"),
);
if (currentFileContent === generatedFileContent) {
console.log("env.d.ts is up to date");
return;
}
const isCI = process.env["IS_CI"] === "true";
const vcsCommand = isCI ? "git" : "sl";
spawn({
cmd: [vcsCommand, "diff", "src/types/env.d.ts"],
stdout: "inherit",
});
// add 1 second to make sure spawn completes
await sleep(1000);
throw new Error("env.d.ts is out of date");
});

View File

@@ -0,0 +1,54 @@
import { env } from "cloudflare:workers";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getUser } from "./getUser";
describe("getUser service", () => {
const mockStub = {
getUserProfile: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
idFromName: vi.fn().mockReturnValue("global-id"),
get: vi.fn().mockReturnValue(mockStub),
};
});
it("should fetch user profile from Durable Object", async () => {
const mockUser = { id: 1, name: "User", statistics: { anime: {} } };
mockStub.getUserProfile.mockResolvedValue(mockUser);
const result = await getUser("token");
expect(result).toEqual({
...mockUser,
statistics: mockUser.statistics.anime,
});
expect(mockStub.getUserProfile).toHaveBeenCalledWith("token");
});
it("should return null if DO throws 401 error", async () => {
mockStub.getUserProfile.mockRejectedValue(new Error("401 Unauthorized"));
const result = await getUser("token");
expect(result).toBeNull();
});
it("should rethrow other DO errors", async () => {
mockStub.getUserProfile.mockRejectedValue(new Error("Other Error"));
await expect(getUser("token")).rejects.toThrow("Other Error");
});
it("should return null if DO returns null", async () => {
mockStub.getUserProfile.mockResolvedValue(null);
const result = await getUser("token");
expect(result).toBeNull();
});
});

View File

@@ -0,0 +1,64 @@
import { env } from "cloudflare:workers";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { markEpisodeAsWatched } from "./anilist";
describe("markEpisodeAsWatched service", () => {
const mockStub = {
markTitleAsWatched: vi.fn(),
markEpisodeAsWatched: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
idFromName: vi.fn().mockReturnValue("global-id"),
get: vi.fn().mockReturnValue(mockStub),
};
});
it("should call markTitleAsWatched on DO if markTitleAsComplete is true", async () => {
mockStub.markTitleAsWatched.mockResolvedValue({
user: { id: 1, statistics: { anime: {} } },
});
const result = await markEpisodeAsWatched("token", 1, 12, true);
expect(result).toBeDefined();
expect(mockStub.markTitleAsWatched).toHaveBeenCalledWith(1, "token");
expect(mockStub.markEpisodeAsWatched).not.toHaveBeenCalled();
});
it("should call markEpisodeAsWatched on DO if markTitleAsComplete is false", async () => {
mockStub.markEpisodeAsWatched.mockResolvedValue({
user: { id: 1, statistics: { anime: {} } },
});
const result = await markEpisodeAsWatched("token", 1, 12, false);
expect(result).toBeDefined();
expect(mockStub.markEpisodeAsWatched).toHaveBeenCalledWith(1, 12, "token");
expect(mockStub.markTitleAsWatched).not.toHaveBeenCalled();
});
it("should throw error if DO returns null", async () => {
mockStub.markEpisodeAsWatched.mockResolvedValue(null);
await expect(markEpisodeAsWatched("token", 1, 12, false)).rejects.toThrow(
"Failed to mark episode as watched",
);
});
it("should return formatted user data", async () => {
const mockUser = { id: 1, statistics: { anime: { count: 10 } } };
mockStub.markEpisodeAsWatched.mockResolvedValue({ user: mockUser });
const result = await markEpisodeAsWatched("token", 1, 12, false);
expect(result).toEqual({
...mockUser,
statistics: { count: 10 },
});
});
});

View File

@@ -0,0 +1,84 @@
import { env } from "cloudflare:workers";
import { describe, expect, it, vi } from "vitest";
import { fetchPopularTitlesFromAnilist } from "./anilist";
// Mock getCurrentAndNextSeason
vi.mock("~/libs/getCurrentAndNextSeason", () => ({
getCurrentAndNextSeason: vi.fn(() => ({
current: { season: "WINTER", year: 2024 },
next: { season: "SPRING", year: 2024 },
})),
}));
vi.mock("../mapTitle", () => ({
mapTitle: vi.fn((title) => ({ id: title.id, title: title.title })),
}));
describe("fetchPopularTitlesFromAnilist (Browse)", () => {
it("should fetch popular titles from Durable Object with current/next season info", async () => {
const mockReponse = {
trending: { media: [{ id: 1, title: "Trending" }] },
season: { media: [{ id: 2, title: "Popular" }] },
nextSeason: {
media: [
{
id: 3,
title: "Upcoming",
nextAiringEpisode: { airingAt: 123 },
},
],
},
};
const mockNextSeasonResponse = {
Page: { media: [{ id: 4, title: "Next Season Popular" }] },
};
const mockStub = {
browsePopular: vi.fn().mockResolvedValue(mockReponse),
nextSeasonPopular: vi.fn().mockResolvedValue(mockNextSeasonResponse),
};
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
getByName: vi.fn().mockReturnValue(mockStub),
};
const result = await fetchPopularTitlesFromAnilist(10);
expect(result.trending).toHaveLength(1);
expect(result.popular).toHaveLength(1);
expect(result.upcoming).toHaveLength(1);
expect(mockStub.browsePopular).toHaveBeenCalledWith(
"WINTER",
2024,
"SPRING",
2024,
10,
);
expect(mockStub.nextSeasonPopular).toHaveBeenCalledWith("SPRING", 2024, 10);
});
it("should handle missing next season data gracefully (return only trending/popular)", async () => {
const mockReponse = {
trending: { media: [{ id: 1, title: "Trending" }] },
season: { media: [{ id: 2, title: "Popular" }] },
nextSeason: { media: [] },
};
const mockStub = {
browsePopular: vi.fn().mockResolvedValue(mockReponse),
};
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
getByName: vi.fn().mockReturnValue(mockStub),
};
const result = await fetchPopularTitlesFromAnilist(10);
expect(result.trending).toHaveLength(1);
expect(result.popular).toHaveLength(1);
expect(result.upcoming).toBeUndefined(); // Or check implementation if it returns empty array or undefined
});
});

View File

@@ -0,0 +1,90 @@
import { env } from "cloudflare:workers";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchPopularTitlesFromAnilist } from "./anilist";
// Mock getCurrentAndNextSeason
vi.mock("~/libs/getCurrentAndNextSeason", () => ({
getCurrentAndNextSeason: vi.fn(() => ({
current: { season: "WINTER", year: 2024 },
next: { season: "SPRING", year: 2024 },
})),
}));
vi.mock("../mapTitle", () => ({
mapTitle: vi.fn((title) => ({ id: title.id, title: title.title })),
}));
describe("fetchPopularTitlesFromAnilist (Category)", () => {
const mockStub = {
getTrendingTitles: vi.fn(),
getPopularTitles: vi.fn(),
nextSeasonPopular: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
// @ts-expect-error - Partial mock
env.ANILIST_DO = {
idFromName: vi.fn().mockReturnValue("global-id"),
get: vi.fn().mockReturnValue(mockStub),
};
});
it("should fetch 'trending' titles from Durable Object", async () => {
mockStub.getTrendingTitles.mockResolvedValue({
media: [{ id: 1, title: "Trending" }],
pageInfo: { hasNextPage: true },
});
const result = await fetchPopularTitlesFromAnilist("trending", 1, 10);
expect(result.results).toHaveLength(1);
expect(result.hasNextPage).toBe(true);
expect(mockStub.getTrendingTitles).toHaveBeenCalledWith(1, 10);
});
it("should fetch 'popular' titles from Durable Object", async () => {
mockStub.getPopularTitles.mockResolvedValue({
media: [{ id: 2, title: "Popular" }],
pageInfo: { hasNextPage: false },
});
const result = await fetchPopularTitlesFromAnilist("popular", 1, 10);
expect(result.results).toHaveLength(1);
expect(mockStub.getPopularTitles).toHaveBeenCalledWith(
1,
10,
"WINTER",
2024,
);
});
it("should fetch 'upcoming' titles from Durable Object", async () => {
mockStub.nextSeasonPopular.mockResolvedValue({
media: [{ id: 3, title: "Upcoming" }],
pageInfo: { hasNextPage: true },
});
const result = await fetchPopularTitlesFromAnilist("upcoming", 1, 10);
expect(result.results).toHaveLength(1);
expect(mockStub.nextSeasonPopular).toHaveBeenCalledWith("SPRING", 2024, 10);
});
it("should throw error for unknown category", async () => {
await expect(
fetchPopularTitlesFromAnilist("unknown" as any, 1, 10),
).rejects.toThrow("Unknown category: unknown");
});
it("should return empty results if DO returns null", async () => {
mockStub.getTrendingTitles.mockResolvedValue(null);
const result = await fetchPopularTitlesFromAnilist("trending", 1, 10);
expect(result.results).toEqual([]);
expect(result.hasNextPage).toBe(false);
});
});

View File

@@ -0,0 +1,72 @@
import { describe, expect, it, vi } from "vitest";
import { maybeUpdateWatchStatusOnAnilist } from "./anilist";
const mockRequest = vi.fn();
vi.mock("graphql-request", () => ({
GraphQLClient: vi.fn(() => ({
request: mockRequest,
})),
}));
vi.mock("~/libs/errors/TitleNotFound", () => ({
AnilistTitleNotFoundError: class extends Error {
constructor() {
super("AnilistTitleNotFoundError");
}
},
}));
describe("maybeUpdateWatchStatusOnAnilist service", () => {
it("should return true immediately if token is missing", async () => {
const result = await maybeUpdateWatchStatusOnAnilist(
1,
"CURRENT",
undefined,
);
expect(result).toBe(true);
});
it("should perform SaveMediaListEntry mutation if watch status is provided", async () => {
mockRequest.mockResolvedValue({ SaveMediaListEntry: { id: 123 } });
const result = await maybeUpdateWatchStatusOnAnilist(1, "CURRENT", "token");
expect(result).toBe(true);
expect(mockRequest).toHaveBeenCalledWith(
expect.anything(),
{ titleId: 1, watchStatus: "CURRENT" },
expect.anything(),
);
});
it("should perform DeleteMediaListEntry if watch status is null", async () => {
mockRequest
.mockResolvedValueOnce({ Media: { mediaListEntry: { id: 456 } } }) // Fetch ID
.mockResolvedValueOnce({ DeleteMediaListEntry: { deleted: true } }); // Delete
const result = await maybeUpdateWatchStatusOnAnilist(1, null, "token");
expect(result).toBe(true);
// First call to get ID
expect(mockRequest).toHaveBeenCalledWith(
expect.anything(),
{ titleId: 1 },
expect.anything(),
);
// Second call to delete
expect(mockRequest).toHaveBeenCalledWith(
expect.anything(),
{ entryId: 456 },
expect.anything(),
);
});
it("should throw AnilistTitleNotFoundError if trying to delete non-existent entry", async () => {
mockRequest.mockResolvedValueOnce({ Media: { mediaListEntry: null } });
await expect(
maybeUpdateWatchStatusOnAnilist(1, null, "token"),
).rejects.toThrow("AnilistTitleNotFoundError");
});
});

View File

@@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"types": ["@cloudflare/workers-types"],
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"~/*": ["src/*"] "~/*": ["src/*"]

View File

@@ -1,8 +1,23 @@
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; import {
defineWorkersProject,
readD1Migrations,
} from "@cloudflare/vitest-pool-workers/config";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { configDefaults } from "vitest/config"; import { configDefaults } from "vitest/config";
export default defineWorkersConfig({ import path from "node:path";
export default defineWorkersProject(async () => {
const migrationsPath = path.join(__dirname, "drizzle");
let migrations: Awaited<ReturnType<typeof readD1Migrations>>;
try {
migrations = await readD1Migrations(migrationsPath);
} catch (e) {
console.warn("Could not read migrations", e);
migrations = [];
}
return {
plugins: [tsconfigPaths()], plugins: [tsconfigPaths()],
test: { test: {
globals: false, globals: false,
@@ -14,7 +29,7 @@ export default defineWorkersConfig({
}, },
coverage: { coverage: {
...configDefaults.coverage, ...configDefaults.coverage,
provider: "v8", provider: "istanbul",
reporter: ["text", "json", "html"], reporter: ["text", "json", "html"],
exclude: [ exclude: [
...configDefaults.coverage.exclude, ...configDefaults.coverage.exclude,
@@ -28,4 +43,5 @@ export default defineWorkersConfig({
], ],
}, },
}, },
};
}); });