From 243701a5c55417342275cae52bec13cf1bd9ec1d Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 3 Jan 2026 19:46:28 +0100 Subject: [PATCH] Use real dates instead of iso string everywhere --- api/.dockerignore | 1 + api/bun.lock | 52 ++++++++----------- api/bunfig.toml | 3 ++ api/package.json | 12 ++--- api/src/auth.ts | 4 +- api/src/base.ts | 13 ++--- api/src/controllers/entries.ts | 6 +-- api/src/controllers/profiles/history.ts | 2 +- api/src/controllers/profiles/watchlist.ts | 1 + api/src/controllers/seed/insert/collection.ts | 2 +- api/src/controllers/seed/refresh.ts | 2 +- api/src/controllers/shows/collections.ts | 2 +- api/src/controllers/shows/logic.ts | 14 +---- api/src/controllers/shows/movies.ts | 2 +- api/src/controllers/shows/series.ts | 2 +- api/src/controllers/videos.ts | 17 +----- api/src/db/schema/entries.ts | 15 +++--- api/src/db/schema/history.ts | 8 ++- api/src/db/schema/images.ts | 17 +++--- api/src/db/schema/mqueue.ts | 16 +++--- api/src/db/schema/seasons.ts | 13 +++-- api/src/db/schema/shows.ts | 13 +++-- api/src/db/schema/staff.ts | 11 ++-- api/src/db/schema/studios.ts | 11 ++-- api/src/db/schema/utils.ts | 19 +------ api/src/db/schema/videos.ts | 11 ++-- api/src/db/schema/watchlist.ts | 18 +++---- api/src/db/utils.ts | 19 +++---- api/src/models/collections.ts | 2 +- api/src/models/entry/base-entry.ts | 2 +- api/src/models/examples/bubble.ts | 4 +- api/src/models/examples/dune-1984.ts | 4 +- api/src/models/examples/dune-2021.ts | 4 +- api/src/models/examples/made-in-abyss.ts | 4 +- api/src/models/history.ts | 2 +- api/src/models/movie.ts | 2 +- api/src/models/season.ts | 2 +- api/src/models/serie.ts | 2 +- api/src/models/utils/db-metadata.ts | 4 +- api/src/models/utils/sort.ts | 6 +-- api/src/models/watchlist.ts | 4 +- api/src/utils.ts | 2 +- api/tests/movies/watchstatus.test.ts | 14 ++--- api/tests/series/get-series.test.ts | 9 ++++ api/tests/series/history.test.ts | 14 ++--- api/tests/series/nextup.test.ts | 14 ++--- 46 files changed, 176 insertions(+), 225 deletions(-) diff --git a/api/.dockerignore b/api/.dockerignore index 6eae1707..51ba0907 100644 --- a/api/.dockerignore +++ b/api/.dockerignore @@ -1,6 +1,7 @@ ** !/package.json !/bun.lock +!/bunfig.toml !/tsconfig.json !/patches !/src diff --git a/api/bun.lock b/api/bun.lock index 8690771d..35dd0ac8 100644 --- a/api/bun.lock +++ b/api/bun.lock @@ -5,7 +5,7 @@ "": { "name": "api", "dependencies": { - "@elysiajs/opentelemetry": "^1.4.9", + "@elysiajs/opentelemetry": "^1.4.10", "@elysiajs/swagger": "zoriya/elysia-swagger#build", "@kubiks/otel-drizzle": "zoriya/drizzle-otel#build", "@logtape/logtape": "^1.3.5", @@ -29,20 +29,20 @@ "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.38.0", - "@types/bun": "^1.3.1", + "@types/bun": "^1.3.5", "blurhash": "^2.0.5", "drizzle-kit": "^0.31.5", "drizzle-orm": "0.44.7", - "elysia": "^1.4.14", - "jose": "^6.1.0", + "elysia": "zoriya/elysia#build", + "jose": "^6.1.3", "node-addon-api": "^8.5.0", "parjs": "^1.3.9", "pg": "^8.16.3", "sharp": "^0.34.4", }, "devDependencies": { - "@biomejs/biome": "2.3.10", - "@types/pg": "^8.15.5", + "@biomejs/biome": "2.3.11", + "@types/pg": "^8.16.0", }, }, }, @@ -50,27 +50,27 @@ "drizzle-orm@0.44.7": "patches/drizzle-orm@0.44.7.patch", }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="], + "@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.9", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Pb/q45K3cm6W6Pc4GrY7bRr519EypSztBhfFDdldCkIvhrnbRXwSlK54f16FCYMwzdbTw1jTsCHKS2VbTyLsMQ=="], + "@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.10", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-2GH187Rr3n3Rq+R7fogn/jcmdwWk9OMtbYhnaJg5ydiLOJvtrztDp0p+zbyGFG2gspx8U9vpaCvSJ69Aq1zZkA=="], "@elysiajs/swagger": ["@elysiajs/swagger@github:zoriya/elysia-swagger#f88fbc7", { "dependencies": { "@scalar/themes": "^0.9.81", "@scalar/types": "^0.1.3", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "zoriya-elysia-swagger-f88fbc7"], @@ -280,13 +280,11 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="], - "@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="], - - "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], @@ -304,7 +302,7 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "char-info": ["char-info@0.3.5", "", { "dependencies": { "node-interval-tree": "^1.3.3" } }, "sha512-gRslEBFEcuLMGLNO1EFIrdN1MMUfO+aqa7y8iWzNyAzB3mYKnTIvP+ioW3jpyeEvqA5WapVLIPINGtFjEIH4cQ=="], @@ -318,8 +316,6 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -328,7 +324,7 @@ "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], - "elysia": ["elysia@1.4.14", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-hM/aCCVgjmLwXje2GFCgKttXPPhrddGsXUVyILKHnfifk/aq8BhtC16EvCg2PSGDAYfZEj9xfwumG7mnyzGkQA=="], + "elysia": ["elysia@github:zoriya/elysia#b547b37", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "exact-mirror": ">= 0.0.9", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "zoriya-elysia-b547b37"], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -338,7 +334,7 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="], + "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], @@ -364,7 +360,7 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="], + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -400,7 +396,7 @@ "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], - "pg-protocol": ["pg-protocol@1.10.0", "", {}, "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q=="], + "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], "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=="], @@ -520,8 +516,6 @@ "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg=="], - "pg/pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], diff --git a/api/bunfig.toml b/api/bunfig.toml index 8370a015..ddeab0ce 100644 --- a/api/bunfig.toml +++ b/api/bunfig.toml @@ -1,2 +1,5 @@ +[console] +depth = 10 + [test] preload = ["./tests/setup.ts"] diff --git a/api/package.json b/api/package.json index 1383393f..68b89ec4 100644 --- a/api/package.json +++ b/api/package.json @@ -9,7 +9,7 @@ "format": "biome check --write ." }, "dependencies": { - "@elysiajs/opentelemetry": "^1.4.9", + "@elysiajs/opentelemetry": "^1.4.10", "@elysiajs/swagger": "zoriya/elysia-swagger#build", "@kubiks/otel-drizzle": "zoriya/drizzle-otel#build", "@logtape/logtape": "^1.3.5", @@ -33,20 +33,20 @@ "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.38.0", - "@types/bun": "^1.3.1", + "@types/bun": "^1.3.5", "blurhash": "^2.0.5", "drizzle-kit": "^0.31.5", "drizzle-orm": "0.44.7", - "elysia": "^1.4.14", - "jose": "^6.1.0", + "elysia": "zoriya/elysia#build", + "jose": "^6.1.3", "node-addon-api": "^8.5.0", "parjs": "^1.3.9", "pg": "^8.16.3", "sharp": "^0.34.4" }, "devDependencies": { - "@biomejs/biome": "2.3.10", - "@types/pg": "^8.15.5" + "@biomejs/biome": "2.3.11", + "@types/pg": "^8.16.0" }, "module": "src/index.js", "patchedDependencies": { diff --git a/api/src/auth.ts b/api/src/auth.ts index c5c28728..10cb1fb0 100644 --- a/api/src/auth.ts +++ b/api/src/auth.ts @@ -94,8 +94,8 @@ const User = t.Object({ id: t.String({ format: "uuid" }), username: t.String(), email: t.String({ format: "email" }), - createdDate: t.String({ format: "date-time" }), - lastSeen: t.String({ format: "date-time" }), + createdDate: t.Date(), + lastSeen: t.Date(), claims: t.Record(t.String(), t.Any()), oidc: t.Record( t.String(), diff --git a/api/src/base.ts b/api/src/base.ts index 5f099907..ee8f608a 100644 --- a/api/src/base.ts +++ b/api/src/base.ts @@ -34,24 +34,17 @@ export const base = new Elysia({ name: "base" }) } catch {} } if (code === "VALIDATION") { - const details = JSON.parse(error.message); - if (details.code === "KError") { - const { code, ...ret } = details; - return ret; - } - details.errors = details.errors.map((x: any) => { - const { schema, ...err } = x; - return err; - }); + const { schema, ...details } = error as any; return { status: error.status, - message: `Validation error on ${details.on}.`, + message: `Validation error.`, details: details, } as KError; } if (code === "NOT_FOUND") { return error; } + console.error(code, error); logger.error("Elysia encountered an error. code={code} error={error}", { code: code, error: error, diff --git a/api/src/controllers/entries.ts b/api/src/controllers/entries.ts index 21e8de20..aed7bf84 100644 --- a/api/src/controllers/entries.ts +++ b/api/src/controllers/entries.ts @@ -18,7 +18,6 @@ import { jsonbAgg, jsonbBuildObject, jsonbObjectAgg, - normalizeDate, sqlarr, } from "~/db/utils"; import { @@ -173,9 +172,6 @@ const entryRelations = { .select({ json: jsonbBuildObject({ ...getColumns(shows), - createdAt: normalizeDate(shows.createdAt), - updatedAt: normalizeDate(shows.updatedAt), - airDate: shows.startAir, isAvailable: sql`${shows.availableCount} != 0`, ...getColumns(transQ), @@ -232,7 +228,7 @@ export const mapProgress = ({ aliased }: { aliased: boolean }) => { const ret = { time: coalesce(time, sql`0`), percent: coalesce(percent, sql`0`), - playedDate: normalizeDate(playedDate), + playedDate: sql`${playedDate}`, videoId: sql`${videoId}`, }; if (!aliased) return ret; diff --git a/api/src/controllers/profiles/history.ts b/api/src/controllers/profiles/history.ts index 9a780ff6..12646e4f 100644 --- a/api/src/controllers/profiles/history.ts +++ b/api/src/controllers/profiles/history.ts @@ -177,7 +177,7 @@ async function updateWatchlist( histArr: { entryPk: number; percent: number; - playedDate: string; + playedDate: Date; }[], ) { if (histArr.length === 0) return; diff --git a/api/src/controllers/profiles/watchlist.ts b/api/src/controllers/profiles/watchlist.ts index b8db0f4d..3184c790 100644 --- a/api/src/controllers/profiles/watchlist.ts +++ b/api/src/controllers/profiles/watchlist.ts @@ -407,6 +407,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] }) }), response: { 200: t.Intersect([SerieWatchStatus, DbMetadata]), + 401: KError, 404: KError, }, permissions: ["core.read"], diff --git a/api/src/controllers/seed/insert/collection.ts b/api/src/controllers/seed/insert/collection.ts index b804005e..d1b80d1c 100644 --- a/api/src/controllers/seed/insert/collection.ts +++ b/api/src/controllers/seed/insert/collection.ts @@ -18,7 +18,7 @@ export const insertCollection = record( | ({ kind: "movie" } & SeedMovie) | ({ kind: "serie" } & SeedSerie) ) & { - nextRefresh: string; + nextRefresh: Date; }, ) => { if (!collection) return null; diff --git a/api/src/controllers/seed/refresh.ts b/api/src/controllers/seed/refresh.ts index 8dfcb47d..09d5ba6d 100644 --- a/api/src/controllers/seed/refresh.ts +++ b/api/src/controllers/seed/refresh.ts @@ -8,5 +8,5 @@ export const guessNextRefresh = (airDate: Date | string) => { 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); + return ret; }; diff --git a/api/src/controllers/shows/collections.ts b/api/src/controllers/shows/collections.ts index 93b24533..17953dd5 100644 --- a/api/src/controllers/shows/collections.ts +++ b/api/src/controllers/shows/collections.ts @@ -23,8 +23,8 @@ import { processLanguages, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; -import { getShows, showFilters, showSort } from "./logic"; import { toQueryStr } from "~/utils"; +import { getShows, showFilters, showSort } from "./logic"; export const collections = new Elysia({ prefix: "/collections", diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index 11a63b21..7aa3b15e 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -18,7 +18,6 @@ import { jsonbAgg, jsonbBuildObject, jsonbObjectAgg, - normalizeDate, sqlarr, } from "~/db/utils"; import type { Entry } from "~/models/entry"; @@ -47,10 +46,6 @@ export const watchStatusQ = db .select({ ...getColumns(watchlist), percent: sql`${watchlist.seenCount}`.as("percent"), - // needed when watchStatusQ is embedded in a jsonbBuildObject - startedAt: normalizeDate(watchlist.startedAt), - lastPlayedAt: normalizeDate(watchlist.lastPlayedAt), - completedAt: normalizeDate(watchlist.completedAt), }) .from(watchlist) .innerJoin(profiles, eq(watchlist.profilePk, profiles.pk)) @@ -130,7 +125,6 @@ export const showRelations = { .as("translations"); }, studios: ({ languages }: { languages: string[] }) => { - const { pk: _, createdAt, updatedAt, ...studioCol } = getColumns(studios); const studioTransQ = db .selectDistinctOn([studioTranslations.pk]) .from(studioTranslations) @@ -146,10 +140,8 @@ export const showRelations = { json: coalesce( jsonbAgg( jsonbBuildObject({ + ...getColumns(studios), ...studioTrans, - ...studioCol, - createdAt: normalizeDate(createdAt), - updatedAt: normalizeDate(updatedAt), }), ), sql`'[]'::jsonb`, @@ -204,8 +196,6 @@ export const showRelations = { number: entries.episodeNumber, videos: entryVideosQ.videos, progress: mapProgress({ aliased: false }), - createdAt: normalizeDate(entries.createdAt), - updatedAt: normalizeDate(entries.updatedAt), }).as("firstEntry"), }) .from(entries) @@ -228,8 +218,6 @@ export const showRelations = { number: entries.episodeNumber, videos: entryVideosQ.videos, progress: mapProgress({ aliased: false }), - createdAt: normalizeDate(entries.createdAt), - updatedAt: normalizeDate(entries.updatedAt), }).as("nextEntry"), }) .from(entries) diff --git a/api/src/controllers/shows/movies.ts b/api/src/controllers/shows/movies.ts index e3612c40..2487fe84 100644 --- a/api/src/controllers/shows/movies.ts +++ b/api/src/controllers/shows/movies.ts @@ -16,8 +16,8 @@ import { processLanguages, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; -import { getShows, showFilters, showSort } from "./logic"; import { toQueryStr } from "~/utils"; +import { getShows, showFilters, showSort } from "./logic"; export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) .model({ diff --git a/api/src/controllers/shows/series.ts b/api/src/controllers/shows/series.ts index e850feec..d750e5ac 100644 --- a/api/src/controllers/shows/series.ts +++ b/api/src/controllers/shows/series.ts @@ -16,8 +16,8 @@ import { processLanguages, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; -import { getShows, showFilters, showSort } from "./logic"; import { toQueryStr } from "~/utils"; +import { getShows, showFilters, showSort } from "./logic"; export const series = new Elysia({ prefix: "/series", tags: ["series"] }) .model({ diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index e365048a..24b8b28d 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -34,7 +34,6 @@ import { jsonbAgg, jsonbBuildObject, jsonbObjectAgg, - normalizeDate, sqlarr, unnest, unnestValues, @@ -251,7 +250,7 @@ const videoRelations = { json: jsonbBuildObject({ percent: history.percent, time: history.time, - playedDate: normalizeDate(history.playedDate), + playedDate: history.playedDate, videoId: videos.id, }), }) @@ -287,9 +286,6 @@ const videoRelations = { number: entries.episodeNumber, videos: entryVideosQ.videos, progress: mapProgress({ aliased: false }), - createdAt: normalizeDate(entries.createdAt), - updatedAt: normalizeDate(entries.updatedAt), - availableSince: normalizeDate(entries.availableSince), }), ), sql`'[]'::jsonb`, @@ -319,16 +315,11 @@ const videoRelations = { ) .as("t"); - const { startedAt, lastPlayedAt, completedAt, ...watchlistCols } = - getColumns(watchlist); const watchStatusQ = db .select({ watchStatus: jsonbBuildObject({ - ...watchlistCols, + ...getColumns(watchlist), percent: watchlist.seenCount, - startedAt: normalizeDate(startedAt), - lastPlayedAt: normalizeDate(lastPlayedAt), - completedAt: normalizeDate(completedAt), }).as("watchStatus"), }) .from(watchlist) @@ -350,8 +341,6 @@ const videoRelations = { airDate: shows.startAir, kind: sql`${shows.kind}`, isAvailable: sql`${shows.availableCount} != 0`, - createdAt: normalizeDate(shows.createdAt), - updatedAt: normalizeDate(shows.updatedAt), ...(preferOriginal && { poster: sql`coalesce(nullif(${shows.original}->'poster', 'null'::jsonb), ${transQ.poster})`, @@ -405,8 +394,6 @@ function getNextVideoEntry({ number: entries.episodeNumber, videos: entryVideosQ.videos, progress: mapProgress({ aliased: false }), - createdAt: normalizeDate(entries.createdAt), - updatedAt: normalizeDate(entries.updatedAt), }, }).as("json"), }) diff --git a/api/src/db/schema/entries.ts b/api/src/db/schema/entries.ts index 11b665b8..c1c36dd1 100644 --- a/api/src/db/schema/entries.ts +++ b/api/src/db/schema/entries.ts @@ -8,12 +8,13 @@ import { primaryKey, real, text, + timestamp, unique, uuid, varchar, } from "drizzle-orm/pg-core"; import { shows } from "./shows"; -import { image, language, schema, timestamp } from "./utils"; +import { image, language, schema } from "./utils"; import { entryVideoJoin } from "./videos"; export const entryType = schema.enum("entry_type", [ @@ -66,14 +67,12 @@ export const entries = schema.table( externalId: entry_extid(), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp({ withTimezone: true }) .notNull() - .default(sql`now()`), - updatedAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .$onUpdate(() => sql`now()`), - availableSince: timestamp({ withTimezone: true, mode: "iso" }), - nextRefresh: timestamp({ withTimezone: true, mode: "iso" }).notNull(), + .$onUpdate(() => new Date()), + availableSince: timestamp({ withTimezone: true }), + nextRefresh: timestamp({ withTimezone: true }).notNull(), }, (t) => [ unique().on(t.showPk, t.seasonNumber, t.episodeNumber), diff --git a/api/src/db/schema/history.ts b/api/src/db/schema/history.ts index 3df6258d..58410804 100644 --- a/api/src/db/schema/history.ts +++ b/api/src/db/schema/history.ts @@ -1,8 +1,8 @@ import { sql } from "drizzle-orm"; -import { check, index, integer } from "drizzle-orm/pg-core"; +import { check, index, integer, timestamp } from "drizzle-orm/pg-core"; import { entries } from "./entries"; import { profiles } from "./profiles"; -import { schema, timestamp } from "./utils"; +import { schema } from "./utils"; import { videos } from "./videos"; export const history = schema.table( @@ -20,9 +20,7 @@ export const history = schema.table( videoPk: integer().references(() => videos.pk, { onDelete: "set null" }), percent: integer().notNull().default(0), time: integer().notNull().default(0), - playedDate: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .default(sql`now()`), + playedDate: timestamp({ withTimezone: true }).notNull().defaultNow(), }, (t) => [ index("history_play_date").on(t.playedDate.desc()), diff --git a/api/src/db/schema/images.ts b/api/src/db/schema/images.ts index b4b6ec1b..ca42c639 100644 --- a/api/src/db/schema/images.ts +++ b/api/src/db/schema/images.ts @@ -1,6 +1,13 @@ import { sql } from "drizzle-orm"; -import { index, integer, jsonb, text, varchar } from "drizzle-orm/pg-core"; -import { schema, timestamp } from "./utils"; +import { + index, + integer, + jsonb, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; +import { schema } from "./utils"; export const imgStatus = schema.enum("img_status", [ "pending", @@ -19,10 +26,8 @@ export const images = schema.table( priority: integer().notNull().default(0), attempt: integer().notNull().default(0), status: imgStatus().notNull().default("pending"), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .default(sql`now()`), - downloadedAt: timestamp({ withTimezone: true, mode: "iso" }), + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + downloadedAt: timestamp({ withTimezone: true }), }, (t) => [index("imgqueue_sort").on(t.priority, t.attempt, t.createdAt)], ); diff --git a/api/src/db/schema/mqueue.ts b/api/src/db/schema/mqueue.ts index c2498146..331008f6 100644 --- a/api/src/db/schema/mqueue.ts +++ b/api/src/db/schema/mqueue.ts @@ -1,6 +1,12 @@ -import { sql } from "drizzle-orm"; -import { index, integer, jsonb, uuid, varchar } from "drizzle-orm/pg-core"; -import { schema, timestamp } from "./utils"; +import { + index, + integer, + jsonb, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; +import { schema } from "./utils"; export const mqueue = schema.table( "mqueue", @@ -10,9 +16,7 @@ export const mqueue = schema.table( message: jsonb().notNull(), priority: integer().notNull().default(0), attempt: integer().notNull().default(0), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .default(sql`now()`), + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), }, (t) => [index("mqueue_created").on(t.createdAt)], ); diff --git a/api/src/db/schema/seasons.ts b/api/src/db/schema/seasons.ts index a196b355..61a6f27e 100644 --- a/api/src/db/schema/seasons.ts +++ b/api/src/db/schema/seasons.ts @@ -6,12 +6,13 @@ import { jsonb, primaryKey, text, + timestamp, unique, uuid, varchar, } from "drizzle-orm/pg-core"; import { shows } from "./shows"; -import { image, language, schema, timestamp } from "./utils"; +import { image, language, schema } from "./utils"; export const season_extid = () => jsonb() @@ -44,13 +45,11 @@ export const seasons = schema.table( externalId: season_extid(), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp({ withTimezone: true }) .notNull() - .default(sql`now()`), - updatedAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .$onUpdate(() => sql`now()`), - nextRefresh: timestamp({ withTimezone: true, mode: "iso" }).notNull(), + .$onUpdate(() => new Date()), + nextRefresh: timestamp({ withTimezone: true }).notNull(), }, (t) => [ unique().on(t.showPk, t.seasonNumber), diff --git a/api/src/db/schema/shows.ts b/api/src/db/schema/shows.ts index 2fbf466a..bf532aeb 100644 --- a/api/src/db/schema/shows.ts +++ b/api/src/db/schema/shows.ts @@ -9,6 +9,7 @@ import { primaryKey, smallint, text, + timestamp, uuid, varchar, } from "drizzle-orm/pg-core"; @@ -17,7 +18,7 @@ import { entries } from "./entries"; import { seasons } from "./seasons"; import { roles } from "./staff"; import { showStudioJoin } from "./studios"; -import { externalid, image, language, schema, timestamp } from "./utils"; +import { externalid, image, language, schema } from "./utils"; export const showKind = schema.enum("show_kind", [ "serie", @@ -86,13 +87,11 @@ export const shows = schema.table( externalId: externalid(), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp({ withTimezone: true }) .notNull() - .default(sql`now()`), - updatedAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .$onUpdate(() => sql`now()`), - nextRefresh: timestamp({ withTimezone: true, mode: "iso" }).notNull(), + .$onUpdate(() => new Date()), + nextRefresh: timestamp({ withTimezone: true }).notNull(), }, (t) => [ check("rating_valid", sql`${t.rating} between 0 and 100`), diff --git a/api/src/db/schema/staff.ts b/api/src/db/schema/staff.ts index 469bb516..27d7d04e 100644 --- a/api/src/db/schema/staff.ts +++ b/api/src/db/schema/staff.ts @@ -4,12 +4,13 @@ import { integer, jsonb, text, + timestamp, uuid, varchar, } from "drizzle-orm/pg-core"; import type { Character } from "~/models/staff"; import { shows } from "./shows"; -import { externalid, image, schema, timestamp } from "./utils"; +import { externalid, image, schema } from "./utils"; export const roleKind = schema.enum("role_kind", [ "actor", @@ -30,12 +31,10 @@ export const staff = schema.table("staff", { image: image(), externalId: externalid(), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp({ withTimezone: true }) .notNull() - .default(sql`now()`), - updatedAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .$onUpdate(() => sql`now()`), + .$onUpdate(() => new Date()), }); export const roles = schema.table( diff --git a/api/src/db/schema/studios.ts b/api/src/db/schema/studios.ts index ff32323e..21ac2ab1 100644 --- a/api/src/db/schema/studios.ts +++ b/api/src/db/schema/studios.ts @@ -4,11 +4,12 @@ import { integer, primaryKey, text, + timestamp, uuid, varchar, } from "drizzle-orm/pg-core"; import { shows } from "./shows"; -import { externalid, image, language, schema, timestamp } from "./utils"; +import { externalid, image, language, schema } from "./utils"; export const studios = schema.table("studios", { pk: integer().primaryKey().generatedAlwaysAsIdentity(), @@ -16,12 +17,10 @@ export const studios = schema.table("studios", { slug: varchar({ length: 255 }).notNull().unique(), externalId: externalid(), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp({ withTimezone: true }) .notNull() - .default(sql`now()`), - updatedAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .$onUpdate(() => sql`now()`), + .$onUpdate(() => new Date()), }); export const studioTranslations = schema.table( diff --git a/api/src/db/schema/utils.ts b/api/src/db/schema/utils.ts index ebe97480..3f6e92e1 100644 --- a/api/src/db/schema/utils.ts +++ b/api/src/db/schema/utils.ts @@ -1,4 +1,4 @@ -import { customType, jsonb, pgSchema, varchar } from "drizzle-orm/pg-core"; +import { jsonb, pgSchema, varchar } from "drizzle-orm/pg-core"; import type { Image } from "~/models/utils"; export const schema = pgSchema("kyoo"); @@ -20,20 +20,3 @@ export const externalid = () => >() .notNull() .default({}); - -export const timestamp = customType<{ - data: string; - driverData: string; - config: { withTimezone: boolean; precision?: number; mode: "iso" }; -}>({ - dataType(config) { - const precision = config?.precision ? ` (${config.precision})` : ""; - return `timestamp${precision}${config?.withTimezone ? " with time zone" : ""}`; - }, - fromDriver(value: string): string { - if (!value) return value; - // postgres format: 2025-06-22 16:13:37.489301+00 - // what we want: 2025-06-22T16:13:37Z - return `${value.substring(0, 10)}T${value.substring(11, 19)}Z`; - }, -}); diff --git a/api/src/db/schema/videos.ts b/api/src/db/schema/videos.ts index 045e07c4..593c4775 100644 --- a/api/src/db/schema/videos.ts +++ b/api/src/db/schema/videos.ts @@ -5,13 +5,14 @@ import { jsonb, primaryKey, text, + timestamp, unique, uuid, varchar, } from "drizzle-orm/pg-core"; import type { Guess } from "~/models/video"; import { entries } from "./entries"; -import { schema, timestamp } from "./utils"; +import { schema } from "./utils"; export const videos = schema.table( "videos", @@ -24,12 +25,10 @@ export const videos = schema.table( version: integer().notNull().default(1), guess: jsonb().$type().notNull(), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp({ withTimezone: true }) .notNull() - .default(sql`now()`), - updatedAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .$onUpdate(() => sql`now()`), + .$onUpdate(() => new Date()), }, (t) => [ check("part_pos", sql`${t.part} >= 0`), diff --git a/api/src/db/schema/watchlist.ts b/api/src/db/schema/watchlist.ts index 48e699f1..a5f17ccd 100644 --- a/api/src/db/schema/watchlist.ts +++ b/api/src/db/schema/watchlist.ts @@ -1,9 +1,9 @@ import { sql } from "drizzle-orm"; -import { check, integer, primaryKey } from "drizzle-orm/pg-core"; +import { check, integer, primaryKey, timestamp } from "drizzle-orm/pg-core"; import { entries } from "./entries"; import { profiles } from "./profiles"; import { shows } from "./shows"; -import { schema, timestamp } from "./utils"; +import { schema } from "./utils"; export const watchlistStatus = schema.enum("watchlist_status", [ "watching", @@ -29,16 +29,14 @@ export const watchlist = schema.table( score: integer(), - startedAt: timestamp({ withTimezone: true, mode: "iso" }), - lastPlayedAt: timestamp({ withTimezone: true, mode: "iso" }), - completedAt: timestamp({ withTimezone: true, mode: "iso" }), + startedAt: timestamp({ withTimezone: true }), + lastPlayedAt: timestamp({ withTimezone: true }), + completedAt: timestamp({ withTimezone: true }), - createdAt: timestamp({ withTimezone: true, mode: "iso" }) + createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp({ withTimezone: true }) .notNull() - .default(sql`now()`), - updatedAt: timestamp({ withTimezone: true, mode: "iso" }) - .notNull() - .$onUpdate(() => sql`now()`), + .$onUpdate(() => new Date()), }, (t) => [ primaryKey({ columns: [t.profilePk, t.showPk] }), diff --git a/api/src/db/utils.ts b/api/src/db/utils.ts index 8b30dc82..2aa05ec3 100644 --- a/api/src/db/utils.ts +++ b/api/src/db/utils.ts @@ -1,5 +1,5 @@ import { - type Column, + Column, type ColumnsSelection, getTableColumns, is, @@ -12,6 +12,8 @@ import { View, ViewBaseConfig, InferSelectModel, + isTable, + isSQLWrapper, } from "drizzle-orm"; import type { CasingCache } from "drizzle-orm/casing"; import type { AnyMySqlSelect } from "drizzle-orm/mysql-core"; @@ -162,10 +164,11 @@ export const unnestValues = < .select( Object.fromEntries([ ...keys.map((x) => [x, sql.raw(`"${dbNames[x]}"`)]), - ...computed.map((x) => [ - x, - (columns[x].defaultFn?.() ?? columns[x].onUpdateFn!()).as(dbNames[x]), - ]), + ...computed.map((x) => { + let def = columns[x].defaultFn?.() ?? columns[x].onUpdateFn!(); + if (!isSQLWrapper(def)) def = sql`${def}`; + return [x, def.as(dbNames[x])]; + }), ]) as { [k in keyof typeof typeInfo.$inferInsert]-?: SQL.Aliased< (typeof typeInfo.$inferInsert)[k] @@ -255,9 +258,3 @@ export const isUniqueConstraint = (e: unknown): boolean => { cause.code === "23505" ); }; - -export const normalizeDate = (date: Column) => { - return sql`to_char(${date}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`.as( - date.name, - ); -}; diff --git a/api/src/models/collections.ts b/api/src/models/collections.ts index e0c709cf..05a37b68 100644 --- a/api/src/models/collections.ts +++ b/api/src/models/collections.ts @@ -28,7 +28,7 @@ const BaseCollection = t.Object({ descrpition: "Date of the last item of the collection", }), ), - nextRefresh: t.String({ format: "date-time" }), + nextRefresh: t.Date(), externalId: ExternalId(), }); diff --git a/api/src/models/entry/base-entry.ts b/api/src/models/entry/base-entry.ts index 2a59b32b..4bca8c0b 100644 --- a/api/src/models/entry/base-entry.ts +++ b/api/src/models/entry/base-entry.ts @@ -12,7 +12,7 @@ export const BaseEntry = () => ), thumbnail: t.Nullable(Image), - nextRefresh: t.String({ format: "date-time" }), + nextRefresh: t.Date(), }); export const EntryTranslation = () => diff --git a/api/src/models/examples/bubble.ts b/api/src/models/examples/bubble.ts index 498c6e5c..ebb6e7e1 100644 --- a/api/src/models/examples/bubble.ts +++ b/api/src/models/examples/bubble.ts @@ -14,8 +14,8 @@ export const bubbleVideo: Video = { from: "guessit", history: [], }, - createdAt: "2024-11-23T15:01:24.968Z", - updatedAt: "2024-11-23T15:01:24.968Z", + createdAt: new Date("2024-11-23T15:01:24.968Z"), + updatedAt: new Date("2024-11-23T15:01:24.968Z"), }; export const bubble: SeedMovie = { diff --git a/api/src/models/examples/dune-1984.ts b/api/src/models/examples/dune-1984.ts index d64dc06e..f0c2e270 100644 --- a/api/src/models/examples/dune-1984.ts +++ b/api/src/models/examples/dune-1984.ts @@ -14,8 +14,8 @@ export const dune1984Video: Video = { from: "guessit", history: [], }, - createdAt: "2024-12-02T11:45:12.968Z", - updatedAt: "2024-12-02T11:45:12.968Z", + createdAt: new Date("2024-12-02T11:45:12.968Z"), + updatedAt: new Date("2024-12-02T11:45:12.968Z"), }; export const dune1984: SeedMovie = { diff --git a/api/src/models/examples/dune-2021.ts b/api/src/models/examples/dune-2021.ts index f8e32e72..ba258dfd 100644 --- a/api/src/models/examples/dune-2021.ts +++ b/api/src/models/examples/dune-2021.ts @@ -14,8 +14,8 @@ export const duneVideo: Video = { from: "guessit", history: [], }, - createdAt: "2024-12-02T10:10:24.968Z", - updatedAt: "2024-12-02T10:10:24.968Z", + createdAt: new Date("2024-12-02T10:10:24.968Z"), + updatedAt: new Date("2024-12-02T10:10:24.968Z"), }; export const dune: SeedMovie = { diff --git a/api/src/models/examples/made-in-abyss.ts b/api/src/models/examples/made-in-abyss.ts index ea3ade33..8cdbc2d0 100644 --- a/api/src/models/examples/made-in-abyss.ts +++ b/api/src/models/examples/made-in-abyss.ts @@ -14,8 +14,8 @@ export const madeInAbyssVideo: Video = { from: "guessit", history: [], }, - createdAt: "2024-11-23T15:01:24.968Z", - updatedAt: "2024-11-23T15:01:24.968Z", + createdAt: new Date("2024-11-23T15:01:24.968Z"), + updatedAt: new Date("2024-11-23T15:01:24.968Z"), }; export const madeInAbyss = { diff --git a/api/src/models/history.ts b/api/src/models/history.ts index 34064ff0..1cbaf350 100644 --- a/api/src/models/history.ts +++ b/api/src/models/history.ts @@ -10,7 +10,7 @@ export const Progress = t.Object({ This value is null if the entry was never watched or is finished. `, }), - playedDate: t.Nullable(t.String({ format: "date-time" })), + playedDate: t.Nullable(t.Date()), videoId: t.Nullable( t.String({ format: "uuid", diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts index 02ab43f5..a2d1706e 100644 --- a/api/src/models/movie.ts +++ b/api/src/models/movie.ts @@ -29,7 +29,7 @@ const BaseMovie = t.Object({ t.Number({ minimum: 0, description: "Runtime of the movie in minutes." }), ), airDate: t.Nullable(t.String({ format: "date" })), - nextRefresh: t.String({ format: "date-time" }), + nextRefresh: t.Date(), externalId: ExternalId(), }); diff --git a/api/src/models/season.ts b/api/src/models/season.ts index 709077ac..4ba947a1 100644 --- a/api/src/models/season.ts +++ b/api/src/models/season.ts @@ -12,7 +12,7 @@ export const BaseSeason = t.Object({ startAir: t.Nullable(t.String({ format: "date" })), endAir: t.Nullable(t.String({ format: "date" })), - nextRefresh: t.String({ format: "date-time" }), + nextRefresh: t.Date(), externalId: SeasonId, }); diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 5cfdea63..34fb9461 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -39,7 +39,7 @@ const BaseSerie = t.Object({ ), startAir: t.Nullable(t.String({ format: "date" })), endAir: t.Nullable(t.String({ format: "date" })), - nextRefresh: t.String({ format: "date-time" }), + nextRefresh: t.Date(), externalId: ExternalId(), }); diff --git a/api/src/models/utils/db-metadata.ts b/api/src/models/utils/db-metadata.ts index ea2f9b7a..76376919 100644 --- a/api/src/models/utils/db-metadata.ts +++ b/api/src/models/utils/db-metadata.ts @@ -1,6 +1,6 @@ import { t } from "elysia"; export const DbMetadata = t.Object({ - createdAt: t.String({ format: "date-time" }), - updatedAt: t.String({ format: "date-time" }), + createdAt: t.Date(), + updatedAt: t.Date(), }); diff --git a/api/src/models/utils/sort.ts b/api/src/models/utils/sort.ts index f61f0bee..70bbd6f1 100644 --- a/api/src/models/utils/sort.ts +++ b/api/src/models/utils/sort.ts @@ -41,12 +41,12 @@ export const Sort = ( .Transform( t.Array( t.Union([ + t.TemplateLiteral("random:${number}"), t.UnionEnum([ + "random", ...Object.keys(values), ...Object.keys(values).map((x) => `-${x}`), - "random", - ] as any), - t.TemplateLiteral("random:${number}"), + ]), ]), { default: def, diff --git a/api/src/models/watchlist.ts b/api/src/models/watchlist.ts index a21b3b22..cf095562 100644 --- a/api/src/models/watchlist.ts +++ b/api/src/models/watchlist.ts @@ -12,8 +12,8 @@ export type WatchlistStatus = typeof WatchlistStatus.static; export const SerieWatchStatus = t.Object({ status: WatchlistStatus, score: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), - startedAt: t.Nullable(t.String({ format: "date-time" })), - completedAt: t.Nullable(t.String({ format: "date-time" })), + startedAt: t.Nullable(t.Date()), + completedAt: t.Nullable(t.Date()), seenCount: t.Integer({ description: "The number of episodes you watched in this serie.", minimum: 0, diff --git a/api/src/utils.ts b/api/src/utils.ts index 80c06d87..fddffad5 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -14,7 +14,7 @@ export function getYear(date: string) { } export type Prettify = { - [K in keyof T]: Prettify; + [K in keyof T]: T[K]; } & {}; // Returns either a filesystem-backed file, or a S3-backed file, diff --git a/api/tests/movies/watchstatus.test.ts b/api/tests/movies/watchstatus.test.ts index ee5ec600..60f49198 100644 --- a/api/tests/movies/watchstatus.test.ts +++ b/api/tests/movies/watchstatus.test.ts @@ -25,7 +25,7 @@ describe("Set & get watch status", () => { const [r, b] = await setMovieStatus(bubble.slug, { status: "completed", - completedAt: "2024-12-21", + completedAt: new Date("2024-12-21"), score: 85, }); expectStatus(r, b).toBe(200); @@ -36,7 +36,7 @@ describe("Set & get watch status", () => { expect(body.items[0].slug).toBe(bubble.slug); expect(body.items[0].watchStatus).toMatchObject({ status: "completed", - completedAt: "2024-12-21T00:00:00Z", + completedAt: "2024-12-21T00:00:00.000Z", score: 85, percent: 100, }); @@ -50,7 +50,7 @@ describe("Set & get watch status", () => { const [r, b] = await setMovieStatus(bubble.slug, { status: "rewatching", // we still need to specify all values - completedAt: "2024-12-21", + completedAt: new Date("2024-12-21"), score: 85, }); expectStatus(r, b).toBe(200); @@ -61,7 +61,7 @@ describe("Set & get watch status", () => { expect(body.items[0].slug).toBe(bubble.slug); expect(body.items[0].watchStatus).toMatchObject({ status: "rewatching", - completedAt: "2024-12-21T00:00:00Z", + completedAt: "2024-12-21T00:00:00.000Z", score: 85, percent: 0, }); @@ -89,7 +89,7 @@ describe("Set & get watch status", () => { expect(body.items[0].slug).toBe(bubble.slug); expect(body.items[0].watchStatus).toMatchObject({ status: "rewatching", - completedAt: "2024-12-21T00:00:00Z", + completedAt: "2024-12-21T00:00:00.000Z", score: 85, percent: 0, }); @@ -99,7 +99,7 @@ describe("Set & get watch status", () => { const [r, b] = await setMovieStatus(bubble.slug, { status: "rewatching", // we still need to specify all values - completedAt: "2024-12-21", + completedAt: new Date("2024-12-21"), score: 85, }); expectStatus(r, b).toBe(200); @@ -109,7 +109,7 @@ describe("Set & get watch status", () => { expect(body.slug).toBe(bubble.slug); expect(body.watchStatus).toMatchObject({ status: "rewatching", - completedAt: "2024-12-21T00:00:00Z", + completedAt: "2024-12-21T00:00:00.000Z", score: 85, percent: 0, }); diff --git a/api/tests/series/get-series.test.ts b/api/tests/series/get-series.test.ts index 426ce77a..e9c0cb08 100644 --- a/api/tests/series/get-series.test.ts +++ b/api/tests/series/get-series.test.ts @@ -41,5 +41,14 @@ describe("Get series", () => { madeInAbyss.entries[0].translations.en.name, ); expect(body.firstEntry.videos).toBeArrayOfSize(1); + // check that it's an iso datetime + console.log(body.createdAt); + expect(body.createdAt).toMatch( + /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+Z/, + ); + console.log(body.firstEntry.createdAt); + expect(body.firstEntry.createdAt).toMatch( + /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+Z/, + ); }); }); diff --git a/api/tests/series/history.test.ts b/api/tests/series/history.test.ts index 12b7cc2b..d59f5bc3 100644 --- a/api/tests/series/history.test.ts +++ b/api/tests/series/history.test.ts @@ -39,14 +39,14 @@ describe("Set & get history", () => { videoId: madeInAbyssVideo.id, percent: 58, time: 28 * 60 + 12, - playedDate: "2025-02-01", + playedDate: new Date("2025-02-01"), }, { entry: bubble.slug, videoId: null, percent: 100, time: 2 * 60, - playedDate: "2025-02-02", + playedDate: new Date("2025-02-02"), }, ]); expectStatus(r, b).toBe(201); @@ -74,7 +74,7 @@ describe("Set & get history", () => { videoId: madeInAbyssVideo.id, percent: 100, time: 38 * 60, - playedDate: "2025-02-03", + playedDate: new Date("2025-02-03"), }, ]); expectStatus(r, b).toBe(201); @@ -109,7 +109,7 @@ describe("Set & get history", () => { percent: 100, time: 38 * 60, videoId: madeInAbyssVideo.id, - playedDate: "2025-02-03T00:00:00Z", + playedDate: "2025-02-03T00:00:00.000Z", }); }); @@ -122,7 +122,7 @@ describe("Set & get history", () => { percent: 100, time: 38 * 60, videoId: madeInAbyssVideo.id, - playedDate: "2025-02-03T00:00:00Z", + playedDate: "2025-02-03T00:00:00.000Z", }); }); @@ -137,13 +137,13 @@ describe("Set & get history", () => { expect(body.items[0].watchStatus).toMatchObject({ status: "watching", seenCount: 1, - startedAt: "2025-02-01T00:00:00Z", + startedAt: "2025-02-01T00:00:00.000Z", }); expect(body.items[1].slug).toBe(bubble.slug); expect(body.items[1].watchStatus).toMatchObject({ status: "completed", percent: 100, - completedAt: "2025-02-02T00:00:00Z", + completedAt: "2025-02-02T00:00:00.000Z", }); }); }); diff --git a/api/tests/series/nextup.test.ts b/api/tests/series/nextup.test.ts index b10ef224..ce34f600 100644 --- a/api/tests/series/nextup.test.ts +++ b/api/tests/series/nextup.test.ts @@ -40,7 +40,7 @@ describe("nextup", () => { expectStatus(r, b).toBe(200); [r, b] = await setSerieStatus(madeInAbyss.slug, { status: "watching", - startedAt: "2024-12-22", + startedAt: new Date("2024-12-22"), completedAt: null, score: null, }); @@ -90,14 +90,14 @@ describe("nextup", () => { videoId: madeInAbyssVideo.id, percent: 58, time: 28 * 60 + 12, - playedDate: "2025-02-01", + playedDate: new Date("2025-02-01"), }, { entry: bubble.slug, videoId: null, percent: 100, time: 2 * 60, - playedDate: "2025-02-02", + playedDate: new Date("2025-02-02"), }, ]); expectStatus(resp, body).toBe(201); @@ -113,7 +113,7 @@ describe("nextup", () => { percent: 58, time: 28 * 60 + 12, videoId: madeInAbyssVideo.id, - playedDate: "2025-02-01T00:00:00Z", + playedDate: "2025-02-01T00:00:00.000Z", }); [resp, body] = await getMovie(bubble.slug, {}); @@ -121,7 +121,7 @@ describe("nextup", () => { expect(body.watchStatus).toMatchObject({ percent: 100, status: "completed", - completedAt: "2025-02-02T00:00:00Z", + completedAt: "2025-02-02T00:00:00.000Z", }); [resp, body] = await getNextup("me", {}); @@ -132,7 +132,7 @@ describe("nextup", () => { percent: 58, time: 28 * 60 + 12, videoId: madeInAbyssVideo.id, - playedDate: "2025-02-01T00:00:00Z", + playedDate: "2025-02-01T00:00:00.000Z", }); }); @@ -143,7 +143,7 @@ describe("nextup", () => { videoId: madeInAbyssVideo.id, percent: 98, time: 28 * 60 + 12, - playedDate: "2025-02-05", + playedDate: new Date("2025-02-05"), }, ]); expectStatus(resp, body).toBe(201);