diff --git a/bun.lock b/bun.lock index 22c1122d..2ef6e90d 100644 --- a/bun.lock +++ b/bun.lock @@ -98,11 +98,13 @@ "sanitize-html": "^2.16.0", "sharp": "^0.34.1", "steam-session": "*", + "xml2js": "^0.6.2", }, "devDependencies": { "@tsconfig/node20": "^20.1.4", "@types/pngjs": "^6.0.5", "@types/sanitize-html": "^2.16.0", + "@types/xml2js": "^0.4.14", "aws-iot-device-sdk-v2": "^1.21.1", "aws4fetch": "^1.0.20", "mqtt": "^5.10.3", @@ -224,6 +226,7 @@ "@solidjs/router": "^0.15.3", "body-scroll-lock-upgrade": "^1.1.0", "eventsource": "^3.0.5", + "fast-average-color": "9.5.0", "focus-trap": "^7.6.4", "hono": "^4.7.4", "modern-normalize": "^3.0.1", @@ -1547,7 +1550,7 @@ "@types/basic-auth": ["@types/basic-auth@1.1.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-dKcUeixGuZn8pBjcUrf1N7x5K6lWuKuwHHitM2IZ4vwZUDWEhhNtwCWiba8jTA9zn0GQQ+fTFkWpKx8pOU/enw=="], - "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], "@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="], @@ -1651,21 +1654,27 @@ "@types/ws": ["@types/ws@8.18.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.32.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/type-utils": "8.32.1", "@typescript-eslint/utils": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg=="], + "@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.32.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/type-utils": "8.33.0", "@typescript-eslint/utils": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.32.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.0", "@typescript-eslint/types": "^8.33.0", "debug": "^4.3.4" } }, "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0" } }, "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.0", "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.0", "@typescript-eslint/tsconfig-utils": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ=="], "@typescript/lib-dom": ["@types/web@0.0.115", "", {}, "sha512-IBtUgtxnITC7WTCg4tv6kCnSP0T+fM+3PzQPIzLzJY1DDlhBFKM/9+uMURw14YweWPDiFNIZ94Gc1bJtwow97g=="], @@ -2389,7 +2398,7 @@ "eslint-plugin-jsdoc": ["eslint-plugin-jsdoc@50.6.3", "", { "dependencies": { "@es-joy/jsdoccomment": "~0.49.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.3.6", "escape-string-regexp": "^4.0.0", "espree": "^10.1.0", "esquery": "^1.6.0", "parse-imports": "^2.1.1", "semver": "^7.6.3", "spdx-expression-parse": "^4.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-NxbJyt1M5zffPcYZ8Nb53/8nnbIScmiLAMdoe0/FAszwb7lcSiX3iYBTsuF7RV84dZZJC8r3NghomrUXsmWvxQ=="], - "eslint-plugin-qwik": ["eslint-plugin-qwik@1.13.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.12.2", "jsx-ast-utils": "^3.3.5" } }, "sha512-7qF4Sq36KY0a8ktCzZac8Q2BBzjAqLb7Stcjwxh4hwO/AD2CcQIl3ZXGvRM57w9siYlmyegUfvxZBCcuvm/4gA=="], + "eslint-plugin-qwik": ["eslint-plugin-qwik@1.14.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.31.0", "jsx-ast-utils": "^3.3.5" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-N0C8V/6F4EfRdC6EmE7o0VnUod9T16u94C+AD325xA0U4IQoC191uvkGeeJtJ7RXv6UPWz0Evh1aHTfaEILhkg=="], "eslint-plugin-regexp": ["eslint-plugin-regexp@2.7.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "comment-parser": "^1.4.0", "jsdoc-type-pratt-parser": "^4.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", "scslre": "^0.3.0" }, "peerDependencies": { "eslint": ">=8.44.0" } }, "sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA=="], @@ -5333,7 +5342,7 @@ "@types/basic-auth/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - "@types/bun/bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + "@types/bun/bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], "@types/bunyan/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], @@ -5369,6 +5378,8 @@ "@types/ws/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/xml2js/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], @@ -5541,8 +5552,6 @@ "eslint-plugin-jsdoc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], - "eslint-plugin-qwik/@typescript-eslint/utils": ["@typescript-eslint/utils@8.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/types": "8.28.0", "@typescript-eslint/typescript-estree": "8.28.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ=="], - "eslint-plugin-unicorn/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], "espree/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], @@ -6639,6 +6648,8 @@ "@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@types/xml2js/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@vanilla-extract/integration/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], @@ -6761,12 +6772,6 @@ "eslint-plugin-jsdoc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.28.0", "", { "dependencies": { "@typescript-eslint/types": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0" } }, "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw=="], - - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.28.0", "", {}, "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA=="], - - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.28.0", "", { "dependencies": { "@typescript-eslint/types": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA=="], - "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "eslint/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -7795,14 +7800,6 @@ "eslint-plugin-import-x/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.28.0", "", { "dependencies": { "@typescript-eslint/types": "8.28.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg=="], - - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.28.0", "", { "dependencies": { "@typescript-eslint/types": "8.28.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg=="], - - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], @@ -7969,12 +7966,6 @@ "eslint-plugin-import-x/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - - "eslint-plugin-qwik/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/infra/api.ts b/infra/api.ts index 183a8754..27298a0e 100644 --- a/infra/api.ts +++ b/infra/api.ts @@ -1,10 +1,10 @@ import { bus } from "./bus"; import { auth } from "./auth"; import { domain } from "./dns"; +import { secret } from "./secret"; import { cluster } from "./cluster"; import { postgres } from "./postgres"; -import { LibraryQueue } from "./steam"; -import { secret, steamEncryptionKey } from "./secret"; +import { libraryQueue } from "./steam"; export const apiService = new sst.aws.Service("Api", { cluster, @@ -14,8 +14,8 @@ export const apiService = new sst.aws.Service("Api", { bus, auth, postgres, - LibraryQueue, - steamEncryptionKey, + libraryQueue, + secret.SteamApiKey, secret.PolarSecret, secret.PolarWebhookSecret, secret.NestriFamilyMonthly, diff --git a/infra/auth.ts b/infra/auth.ts index 3b7d1c68..37918a84 100644 --- a/infra/auth.ts +++ b/infra/auth.ts @@ -1,8 +1,8 @@ import { bus } from "./bus"; import { domain } from "./dns"; +import { secret } from "./secret"; import { cluster } from "./cluster"; import { postgres } from "./postgres"; -import { secret, steamEncryptionKey } from "./secret"; export const authService = new sst.aws.Service("Auth", { cluster, @@ -13,7 +13,6 @@ export const authService = new sst.aws.Service("Auth", { bus, postgres, secret.PolarSecret, - steamEncryptionKey, secret.GithubClientID, secret.DiscordClientID, secret.GithubClientSecret, diff --git a/infra/bus.ts b/infra/bus.ts index 0827d5fb..7237ca02 100644 --- a/infra/bus.ts +++ b/infra/bus.ts @@ -1,8 +1,8 @@ import { vpc } from "./vpc"; -import { storage } from "./storage"; +import { secret } from "./secret"; // import { email } from "./email"; +import { storage } from "./storage"; import { postgres } from "./postgres"; -import { steamEncryptionKey } from "./secret"; export const bus = new sst.aws.Bus("Bus"); @@ -11,16 +11,25 @@ bus.subscribe("Event", { handler: "packages/functions/src/events/index.handler", link: [ // email, - postgres, + bus, storage, - steamEncryptionKey + postgres, + secret.PolarSecret, + secret.SteamApiKey ], timeout: "10 minutes", memory: "3002 MB",// For faster processing of large(r) images permissions: [ { - actions: ["ses:SendEmail"], + actions: ["ses:SendEmail","sqs:SendMessage"], resources: ["*"], }, ], + // transform: { + // function: { + // deadLetterConfig: { + // targetArn: EventDlq.arn, + // }, + // }, + // }, }); \ No newline at end of file diff --git a/infra/postgres.ts b/infra/postgres.ts index 69a58aa8..0edbebb4 100644 --- a/infra/postgres.ts +++ b/infra/postgres.ts @@ -1,6 +1,5 @@ import { vpc } from "./vpc"; import { isPermanentStage } from "./stage"; -import { steamEncryptionKey } from "./secret"; // TODO: Add a dev db to use, this will help with running zero locally... and testing it export const postgres = new sst.aws.Aurora("Database", { @@ -42,7 +41,7 @@ export const postgres = new sst.aws.Aurora("Database", { new sst.x.DevCommand("Studio", { - link: [postgres, steamEncryptionKey], + link: [postgres], dev: { command: "bun db:dev studio", directory: "packages/core", diff --git a/infra/secret.ts b/infra/secret.ts index c8256a87..ddadb712 100644 --- a/infra/secret.ts +++ b/infra/secret.ts @@ -1,5 +1,6 @@ export const secret = { PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY), + SteamApiKey: new sst.Secret("SteamApiKey"), GithubClientID: new sst.Secret("GithubClientID"), DiscordClientID: new sst.Secret("DiscordClientID"), PolarWebhookSecret: new sst.Secret("PolarWebhookSecret"), @@ -14,17 +15,4 @@ export const secret = { NestriFamilyYearly: new sst.Secret("NestriFamilyYearly"), }; -export const allSecrets = Object.values(secret); - -sst.Linkable.wrap(random.RandomString, (resource) => ({ - properties: { - value: resource.result, - }, -})); - -export const steamEncryptionKey = new random.RandomString( - "SteamEncryptionKey", - { - length: 32, - }, -); \ No newline at end of file +export const allSecrets = Object.values(secret); \ No newline at end of file diff --git a/infra/steam.ts b/infra/steam.ts index 6e246fe5..94d7e9b1 100644 --- a/infra/steam.ts +++ b/infra/steam.ts @@ -1,19 +1,29 @@ +import { bus } from "./bus"; import { vpc } from "./vpc"; +import { secret } from "./secret"; import { postgres } from "./postgres"; -import { steamEncryptionKey } from "./secret"; -export const LibraryQueue = new sst.aws.Queue("LibraryQueue", { - fifo: true, - visibilityTimeout: "10 minutes", +export const libraryDlq = new sst.aws.Queue("LibraryDLQ"); + +export const libraryQueue = new sst.aws.Queue("LibraryQueue", { + dlq: libraryDlq.arn, + visibilityTimeout: "5 minutes", }); -LibraryQueue.subscribe({ +libraryQueue.subscribe({ vpc, - timeout: "10 minutes", memory: "3002 MB", + timeout: "5 minutes", handler: "packages/functions/src/queues/library.handler", link: [ + bus, postgres, - steamEncryptionKey + secret.SteamApiKey + ], + permissions: [ + { + actions: ["sqs:SendMessage"], + resources: ["*"], + }, ], }); \ No newline at end of file diff --git a/packages/core/migrations/0020_vengeful_wallop.sql b/packages/core/migrations/0020_vengeful_wallop.sql new file mode 100644 index 00000000..d066e719 --- /dev/null +++ b/packages/core/migrations/0020_vengeful_wallop.sql @@ -0,0 +1,23 @@ +ALTER TABLE "steam_account_credentials" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint +DROP TABLE "steam_account_credentials" CASCADE;--> statement-breakpoint +ALTER TABLE "game_libraries" RENAME COLUMN "owner_id" TO "owner_steam_id";--> statement-breakpoint +ALTER TABLE "teams" RENAME COLUMN "owner_id" TO "owner_steam_id";--> statement-breakpoint +ALTER TABLE "steam_accounts" DROP CONSTRAINT "idx_steam_username";--> statement-breakpoint +ALTER TABLE "game_libraries" DROP CONSTRAINT "game_libraries_owner_id_steam_accounts_id_fk"; +--> statement-breakpoint +ALTER TABLE "teams" DROP CONSTRAINT "teams_owner_id_users_id_fk"; +--> statement-breakpoint +ALTER TABLE "teams" DROP CONSTRAINT "teams_slug_steam_accounts_username_fk"; +--> statement-breakpoint +DROP INDEX "idx_team_slug";--> statement-breakpoint +DROP INDEX "idx_game_libraries_owner_id";--> statement-breakpoint +ALTER TABLE "game_libraries" DROP CONSTRAINT "game_libraries_base_game_id_owner_id_pk";--> statement-breakpoint +ALTER TABLE "game_libraries" ALTER COLUMN "last_played" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "game_libraries" ADD CONSTRAINT "game_libraries_base_game_id_owner_steam_id_pk" PRIMARY KEY("base_game_id","owner_steam_id");--> statement-breakpoint +ALTER TABLE "game_libraries" ADD CONSTRAINT "game_libraries_owner_steam_id_steam_accounts_id_fk" FOREIGN KEY ("owner_steam_id") REFERENCES "public"."steam_accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "teams" ADD CONSTRAINT "teams_owner_steam_id_steam_accounts_id_fk" FOREIGN KEY ("owner_steam_id") REFERENCES "public"."steam_accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_game_libraries_owner_id" ON "game_libraries" USING btree ("owner_steam_id");--> statement-breakpoint +ALTER TABLE "game_libraries" DROP COLUMN "time_acquired";--> statement-breakpoint +ALTER TABLE "game_libraries" DROP COLUMN "is_family_shared";--> statement-breakpoint +ALTER TABLE "steam_accounts" DROP COLUMN "username";--> statement-breakpoint +ALTER TABLE "teams" DROP COLUMN "slug"; \ No newline at end of file diff --git a/packages/core/migrations/0021_real_skreet.sql b/packages/core/migrations/0021_real_skreet.sql new file mode 100644 index 00000000..142a7891 --- /dev/null +++ b/packages/core/migrations/0021_real_skreet.sql @@ -0,0 +1,2 @@ +ALTER TYPE "public"."category_type" ADD VALUE 'category';--> statement-breakpoint +ALTER TYPE "public"."category_type" ADD VALUE 'franchise'; \ No newline at end of file diff --git a/packages/core/migrations/0022_clean_living_lightning.sql b/packages/core/migrations/0022_clean_living_lightning.sql new file mode 100644 index 00000000..3c82179e --- /dev/null +++ b/packages/core/migrations/0022_clean_living_lightning.sql @@ -0,0 +1,6 @@ +ALTER TABLE "public"."categories" ALTER COLUMN "type" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "public"."games" ALTER COLUMN "type" SET DATA TYPE text;--> statement-breakpoint +DROP TYPE "public"."category_type";--> statement-breakpoint +CREATE TYPE "public"."category_type" AS ENUM('tag', 'genre', 'publisher', 'developer', 'categorie', 'franchise');--> statement-breakpoint +ALTER TABLE "public"."categories" ALTER COLUMN "type" SET DATA TYPE "public"."category_type" USING "type"::"public"."category_type";--> statement-breakpoint +ALTER TABLE "public"."games" ALTER COLUMN "type" SET DATA TYPE "public"."category_type" USING "type"::"public"."category_type"; \ No newline at end of file diff --git a/packages/core/migrations/0023_flawless_steel_serpent.sql b/packages/core/migrations/0023_flawless_steel_serpent.sql new file mode 100644 index 00000000..e2d42b79 --- /dev/null +++ b/packages/core/migrations/0023_flawless_steel_serpent.sql @@ -0,0 +1,2 @@ +ALTER TABLE "base_games" ALTER COLUMN "description" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "base_games" ADD COLUMN "links" text[]; \ No newline at end of file diff --git a/packages/core/migrations/0024_damp_cerise.sql b/packages/core/migrations/0024_damp_cerise.sql new file mode 100644 index 00000000..4b60db3f --- /dev/null +++ b/packages/core/migrations/0024_damp_cerise.sql @@ -0,0 +1 @@ +ALTER TABLE "base_games" ALTER COLUMN "links" SET DATA TYPE json; \ No newline at end of file diff --git a/packages/core/migrations/meta/0020_snapshot.json b/packages/core/migrations/meta/0020_snapshot.json new file mode 100644 index 00000000..094f53d2 --- /dev/null +++ b/packages/core/migrations/meta/0020_snapshot.json @@ -0,0 +1,1158 @@ +{ + "id": "4312f388-81e7-4b19-a9ca-cca92004aaa6", + "prevId": "dc82780b-e403-4f48-8bf1-5b71291d77d8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.base_games": { + "name": "base_games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "release_date": { + "name": "release_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_genre": { + "name": "primary_genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "controller_support": { + "name": "controller_support", + "type": "controller_support", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "compatibility": { + "name": "compatibility", + "type": "compatibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "score": { + "name": "score", + "type": "numeric(2, 1)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_base_games_slug": { + "name": "idx_base_games_slug", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_categories_type": { + "name": "idx_categories_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "categories_slug_type_pk": { + "name": "categories_slug_type_pk", + "columns": [ + "slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends_list": { + "name": "friends_list", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "friend_steam_id": { + "name": "friend_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_friends_list_friend_steam_id": { + "name": "idx_friends_list_friend_steam_id", + "columns": [ + { + "expression": "friend_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_list_steam_id_steam_accounts_id_fk": { + "name": "friends_list_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "friends_list_friend_steam_id_steam_accounts_id_fk": { + "name": "friends_list_friend_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "friend_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "friends_list_steam_id_friend_steam_id_pk": { + "name": "friends_list_steam_id_friend_steam_id_pk", + "columns": [ + "steam_id", + "friend_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.games": { + "name": "games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "category_slug": { + "name": "category_slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_games_category_slug": { + "name": "idx_games_category_slug", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_type": { + "name": "idx_games_category_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_slug_type": { + "name": "idx_games_category_slug_type", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "games_base_game_id_base_games_id_fk": { + "name": "games_base_game_id_base_games_id_fk", + "tableFrom": "games", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "games_categories_fkey": { + "name": "games_categories_fkey", + "tableFrom": "games", + "tableTo": "categories", + "columnsFrom": [ + "category_slug", + "type" + ], + "columnsTo": [ + "slug", + "type" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "games_base_game_id_category_slug_type_pk": { + "name": "games_base_game_id_category_slug_type_pk", + "columns": [ + "base_game_id", + "category_slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.images": { + "name": "images", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "image_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image_hash": { + "name": "image_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dimensions": { + "name": "dimensions", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "extracted_color": { + "name": "extracted_color", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_images_type": { + "name": "idx_images_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_images_game_id": { + "name": "idx_images_game_id", + "columns": [ + { + "expression": "base_game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "images_base_game_id_base_games_id_fk": { + "name": "images_base_game_id_base_games_id_fk", + "tableFrom": "images", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "images_image_hash_type_base_game_id_position_pk": { + "name": "images_image_hash_type_base_game_id_position_pk", + "columns": [ + "image_hash", + "type", + "base_game_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_libraries": { + "name": "game_libraries", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_played": { + "name": "last_played", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_playtime": { + "name": "total_playtime", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_game_libraries_owner_id": { + "name": "idx_game_libraries_owner_id", + "columns": [ + { + "expression": "owner_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_libraries_base_game_id_base_games_id_fk": { + "name": "game_libraries_base_game_id_base_games_id_fk", + "tableFrom": "game_libraries", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_libraries_owner_steam_id_steam_accounts_id_fk": { + "name": "game_libraries_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "game_libraries", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "game_libraries_base_game_id_owner_steam_id_pk": { + "name": "game_libraries_base_game_id_owner_steam_id_pk", + "columns": [ + "base_game_id", + "owner_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_member_steam_id": { + "name": "idx_member_steam_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_member_user_id": { + "name": "idx_member_user_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"members\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_steam_id_steam_accounts_id_fk": { + "name": "members_steam_id_steam_accounts_id_fk", + "tableFrom": "members", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "restrict" + } + }, + "compositePrimaryKeys": { + "members_id_team_id_pk": { + "name": "members_id_team_id_pk", + "columns": [ + "id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.steam_accounts": { + "name": "steam_accounts", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "steam_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "real_name": { + "name": "real_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "member_since": { + "name": "member_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_url": { + "name": "profile_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_hash": { + "name": "avatar_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "steam_accounts_user_id_users_id_fk": { + "name": "steam_accounts_user_id_users_id_fk", + "tableFrom": "steam_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "max_members": { + "name": "max_members", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_steam_id_steam_accounts_id_fk": { + "name": "teams_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "teams", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_team_invite_code": { + "name": "idx_team_invite_code", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "polar_customer_id": { + "name": "polar_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_user_email": { + "name": "idx_user_email", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.compatibility": { + "name": "compatibility", + "schema": "public", + "values": [ + "high", + "mid", + "low", + "unknown" + ] + }, + "public.controller_support": { + "name": "controller_support", + "schema": "public", + "values": [ + "full", + "partial", + "unknown" + ] + }, + "public.category_type": { + "name": "category_type", + "schema": "public", + "values": [ + "tag", + "genre", + "publisher", + "developer" + ] + }, + "public.image_type": { + "name": "image_type", + "schema": "public", + "values": [ + "heroArt", + "icon", + "logo", + "banner", + "poster", + "boxArt", + "screenshot", + "backdrop" + ] + }, + "public.member_role": { + "name": "member_role", + "schema": "public", + "values": [ + "child", + "adult" + ] + }, + "public.steam_status": { + "name": "steam_status", + "schema": "public", + "values": [ + "online", + "offline", + "dnd", + "playing" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/core/migrations/meta/0021_snapshot.json b/packages/core/migrations/meta/0021_snapshot.json new file mode 100644 index 00000000..90115574 --- /dev/null +++ b/packages/core/migrations/meta/0021_snapshot.json @@ -0,0 +1,1160 @@ +{ + "id": "aeb2e0e9-cc3f-418d-b4ae-d4399b3d3ccc", + "prevId": "4312f388-81e7-4b19-a9ca-cca92004aaa6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.base_games": { + "name": "base_games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "release_date": { + "name": "release_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_genre": { + "name": "primary_genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "controller_support": { + "name": "controller_support", + "type": "controller_support", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "compatibility": { + "name": "compatibility", + "type": "compatibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "score": { + "name": "score", + "type": "numeric(2, 1)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_base_games_slug": { + "name": "idx_base_games_slug", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_categories_type": { + "name": "idx_categories_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "categories_slug_type_pk": { + "name": "categories_slug_type_pk", + "columns": [ + "slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends_list": { + "name": "friends_list", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "friend_steam_id": { + "name": "friend_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_friends_list_friend_steam_id": { + "name": "idx_friends_list_friend_steam_id", + "columns": [ + { + "expression": "friend_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_list_steam_id_steam_accounts_id_fk": { + "name": "friends_list_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "friends_list_friend_steam_id_steam_accounts_id_fk": { + "name": "friends_list_friend_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "friend_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "friends_list_steam_id_friend_steam_id_pk": { + "name": "friends_list_steam_id_friend_steam_id_pk", + "columns": [ + "steam_id", + "friend_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.games": { + "name": "games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "category_slug": { + "name": "category_slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_games_category_slug": { + "name": "idx_games_category_slug", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_type": { + "name": "idx_games_category_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_slug_type": { + "name": "idx_games_category_slug_type", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "games_base_game_id_base_games_id_fk": { + "name": "games_base_game_id_base_games_id_fk", + "tableFrom": "games", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "games_categories_fkey": { + "name": "games_categories_fkey", + "tableFrom": "games", + "tableTo": "categories", + "columnsFrom": [ + "category_slug", + "type" + ], + "columnsTo": [ + "slug", + "type" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "games_base_game_id_category_slug_type_pk": { + "name": "games_base_game_id_category_slug_type_pk", + "columns": [ + "base_game_id", + "category_slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.images": { + "name": "images", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "image_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image_hash": { + "name": "image_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dimensions": { + "name": "dimensions", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "extracted_color": { + "name": "extracted_color", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_images_type": { + "name": "idx_images_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_images_game_id": { + "name": "idx_images_game_id", + "columns": [ + { + "expression": "base_game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "images_base_game_id_base_games_id_fk": { + "name": "images_base_game_id_base_games_id_fk", + "tableFrom": "images", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "images_image_hash_type_base_game_id_position_pk": { + "name": "images_image_hash_type_base_game_id_position_pk", + "columns": [ + "image_hash", + "type", + "base_game_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_libraries": { + "name": "game_libraries", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_played": { + "name": "last_played", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_playtime": { + "name": "total_playtime", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_game_libraries_owner_id": { + "name": "idx_game_libraries_owner_id", + "columns": [ + { + "expression": "owner_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_libraries_base_game_id_base_games_id_fk": { + "name": "game_libraries_base_game_id_base_games_id_fk", + "tableFrom": "game_libraries", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_libraries_owner_steam_id_steam_accounts_id_fk": { + "name": "game_libraries_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "game_libraries", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "game_libraries_base_game_id_owner_steam_id_pk": { + "name": "game_libraries_base_game_id_owner_steam_id_pk", + "columns": [ + "base_game_id", + "owner_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_member_steam_id": { + "name": "idx_member_steam_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_member_user_id": { + "name": "idx_member_user_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"members\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_steam_id_steam_accounts_id_fk": { + "name": "members_steam_id_steam_accounts_id_fk", + "tableFrom": "members", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "restrict" + } + }, + "compositePrimaryKeys": { + "members_id_team_id_pk": { + "name": "members_id_team_id_pk", + "columns": [ + "id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.steam_accounts": { + "name": "steam_accounts", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "steam_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "real_name": { + "name": "real_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "member_since": { + "name": "member_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_url": { + "name": "profile_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_hash": { + "name": "avatar_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "steam_accounts_user_id_users_id_fk": { + "name": "steam_accounts_user_id_users_id_fk", + "tableFrom": "steam_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "max_members": { + "name": "max_members", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_steam_id_steam_accounts_id_fk": { + "name": "teams_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "teams", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_team_invite_code": { + "name": "idx_team_invite_code", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "polar_customer_id": { + "name": "polar_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_user_email": { + "name": "idx_user_email", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.compatibility": { + "name": "compatibility", + "schema": "public", + "values": [ + "high", + "mid", + "low", + "unknown" + ] + }, + "public.controller_support": { + "name": "controller_support", + "schema": "public", + "values": [ + "full", + "partial", + "unknown" + ] + }, + "public.category_type": { + "name": "category_type", + "schema": "public", + "values": [ + "tag", + "genre", + "publisher", + "developer", + "category", + "franchise" + ] + }, + "public.image_type": { + "name": "image_type", + "schema": "public", + "values": [ + "heroArt", + "icon", + "logo", + "banner", + "poster", + "boxArt", + "screenshot", + "backdrop" + ] + }, + "public.member_role": { + "name": "member_role", + "schema": "public", + "values": [ + "child", + "adult" + ] + }, + "public.steam_status": { + "name": "steam_status", + "schema": "public", + "values": [ + "online", + "offline", + "dnd", + "playing" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/core/migrations/meta/0022_snapshot.json b/packages/core/migrations/meta/0022_snapshot.json new file mode 100644 index 00000000..d1caa721 --- /dev/null +++ b/packages/core/migrations/meta/0022_snapshot.json @@ -0,0 +1,1160 @@ +{ + "id": "2fecb1ec-996a-46ba-b342-8d351007b25f", + "prevId": "aeb2e0e9-cc3f-418d-b4ae-d4399b3d3ccc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.base_games": { + "name": "base_games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "release_date": { + "name": "release_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_genre": { + "name": "primary_genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "controller_support": { + "name": "controller_support", + "type": "controller_support", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "compatibility": { + "name": "compatibility", + "type": "compatibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "score": { + "name": "score", + "type": "numeric(2, 1)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_base_games_slug": { + "name": "idx_base_games_slug", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_categories_type": { + "name": "idx_categories_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "categories_slug_type_pk": { + "name": "categories_slug_type_pk", + "columns": [ + "slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends_list": { + "name": "friends_list", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "friend_steam_id": { + "name": "friend_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_friends_list_friend_steam_id": { + "name": "idx_friends_list_friend_steam_id", + "columns": [ + { + "expression": "friend_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_list_steam_id_steam_accounts_id_fk": { + "name": "friends_list_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "friends_list_friend_steam_id_steam_accounts_id_fk": { + "name": "friends_list_friend_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "friend_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "friends_list_steam_id_friend_steam_id_pk": { + "name": "friends_list_steam_id_friend_steam_id_pk", + "columns": [ + "steam_id", + "friend_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.games": { + "name": "games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "category_slug": { + "name": "category_slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_games_category_slug": { + "name": "idx_games_category_slug", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_type": { + "name": "idx_games_category_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_slug_type": { + "name": "idx_games_category_slug_type", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "games_base_game_id_base_games_id_fk": { + "name": "games_base_game_id_base_games_id_fk", + "tableFrom": "games", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "games_categories_fkey": { + "name": "games_categories_fkey", + "tableFrom": "games", + "tableTo": "categories", + "columnsFrom": [ + "category_slug", + "type" + ], + "columnsTo": [ + "slug", + "type" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "games_base_game_id_category_slug_type_pk": { + "name": "games_base_game_id_category_slug_type_pk", + "columns": [ + "base_game_id", + "category_slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.images": { + "name": "images", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "image_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image_hash": { + "name": "image_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dimensions": { + "name": "dimensions", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "extracted_color": { + "name": "extracted_color", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_images_type": { + "name": "idx_images_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_images_game_id": { + "name": "idx_images_game_id", + "columns": [ + { + "expression": "base_game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "images_base_game_id_base_games_id_fk": { + "name": "images_base_game_id_base_games_id_fk", + "tableFrom": "images", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "images_image_hash_type_base_game_id_position_pk": { + "name": "images_image_hash_type_base_game_id_position_pk", + "columns": [ + "image_hash", + "type", + "base_game_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_libraries": { + "name": "game_libraries", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_played": { + "name": "last_played", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_playtime": { + "name": "total_playtime", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_game_libraries_owner_id": { + "name": "idx_game_libraries_owner_id", + "columns": [ + { + "expression": "owner_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_libraries_base_game_id_base_games_id_fk": { + "name": "game_libraries_base_game_id_base_games_id_fk", + "tableFrom": "game_libraries", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_libraries_owner_steam_id_steam_accounts_id_fk": { + "name": "game_libraries_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "game_libraries", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "game_libraries_base_game_id_owner_steam_id_pk": { + "name": "game_libraries_base_game_id_owner_steam_id_pk", + "columns": [ + "base_game_id", + "owner_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_member_steam_id": { + "name": "idx_member_steam_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_member_user_id": { + "name": "idx_member_user_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"members\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_steam_id_steam_accounts_id_fk": { + "name": "members_steam_id_steam_accounts_id_fk", + "tableFrom": "members", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "restrict" + } + }, + "compositePrimaryKeys": { + "members_id_team_id_pk": { + "name": "members_id_team_id_pk", + "columns": [ + "id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.steam_accounts": { + "name": "steam_accounts", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "steam_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "real_name": { + "name": "real_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "member_since": { + "name": "member_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_url": { + "name": "profile_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_hash": { + "name": "avatar_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "steam_accounts_user_id_users_id_fk": { + "name": "steam_accounts_user_id_users_id_fk", + "tableFrom": "steam_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "max_members": { + "name": "max_members", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_steam_id_steam_accounts_id_fk": { + "name": "teams_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "teams", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_team_invite_code": { + "name": "idx_team_invite_code", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "polar_customer_id": { + "name": "polar_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_user_email": { + "name": "idx_user_email", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.compatibility": { + "name": "compatibility", + "schema": "public", + "values": [ + "high", + "mid", + "low", + "unknown" + ] + }, + "public.controller_support": { + "name": "controller_support", + "schema": "public", + "values": [ + "full", + "partial", + "unknown" + ] + }, + "public.category_type": { + "name": "category_type", + "schema": "public", + "values": [ + "tag", + "genre", + "publisher", + "developer", + "categorie", + "franchise" + ] + }, + "public.image_type": { + "name": "image_type", + "schema": "public", + "values": [ + "heroArt", + "icon", + "logo", + "banner", + "poster", + "boxArt", + "screenshot", + "backdrop" + ] + }, + "public.member_role": { + "name": "member_role", + "schema": "public", + "values": [ + "child", + "adult" + ] + }, + "public.steam_status": { + "name": "steam_status", + "schema": "public", + "values": [ + "online", + "offline", + "dnd", + "playing" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/core/migrations/meta/0023_snapshot.json b/packages/core/migrations/meta/0023_snapshot.json new file mode 100644 index 00000000..d926cf2a --- /dev/null +++ b/packages/core/migrations/meta/0023_snapshot.json @@ -0,0 +1,1166 @@ +{ + "id": "10e3e27a-30cc-4ef1-9966-a2490571942e", + "prevId": "2fecb1ec-996a-46ba-b342-8d351007b25f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.base_games": { + "name": "base_games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "links": { + "name": "links", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_date": { + "name": "release_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "primary_genre": { + "name": "primary_genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "controller_support": { + "name": "controller_support", + "type": "controller_support", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "compatibility": { + "name": "compatibility", + "type": "compatibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "score": { + "name": "score", + "type": "numeric(2, 1)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_base_games_slug": { + "name": "idx_base_games_slug", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_categories_type": { + "name": "idx_categories_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "categories_slug_type_pk": { + "name": "categories_slug_type_pk", + "columns": [ + "slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends_list": { + "name": "friends_list", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "friend_steam_id": { + "name": "friend_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_friends_list_friend_steam_id": { + "name": "idx_friends_list_friend_steam_id", + "columns": [ + { + "expression": "friend_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_list_steam_id_steam_accounts_id_fk": { + "name": "friends_list_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "friends_list_friend_steam_id_steam_accounts_id_fk": { + "name": "friends_list_friend_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "friend_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "friends_list_steam_id_friend_steam_id_pk": { + "name": "friends_list_steam_id_friend_steam_id_pk", + "columns": [ + "steam_id", + "friend_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.games": { + "name": "games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "category_slug": { + "name": "category_slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_games_category_slug": { + "name": "idx_games_category_slug", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_type": { + "name": "idx_games_category_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_slug_type": { + "name": "idx_games_category_slug_type", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "games_base_game_id_base_games_id_fk": { + "name": "games_base_game_id_base_games_id_fk", + "tableFrom": "games", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "games_categories_fkey": { + "name": "games_categories_fkey", + "tableFrom": "games", + "tableTo": "categories", + "columnsFrom": [ + "category_slug", + "type" + ], + "columnsTo": [ + "slug", + "type" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "games_base_game_id_category_slug_type_pk": { + "name": "games_base_game_id_category_slug_type_pk", + "columns": [ + "base_game_id", + "category_slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.images": { + "name": "images", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "image_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image_hash": { + "name": "image_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dimensions": { + "name": "dimensions", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "extracted_color": { + "name": "extracted_color", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_images_type": { + "name": "idx_images_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_images_game_id": { + "name": "idx_images_game_id", + "columns": [ + { + "expression": "base_game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "images_base_game_id_base_games_id_fk": { + "name": "images_base_game_id_base_games_id_fk", + "tableFrom": "images", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "images_image_hash_type_base_game_id_position_pk": { + "name": "images_image_hash_type_base_game_id_position_pk", + "columns": [ + "image_hash", + "type", + "base_game_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_libraries": { + "name": "game_libraries", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_played": { + "name": "last_played", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_playtime": { + "name": "total_playtime", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_game_libraries_owner_id": { + "name": "idx_game_libraries_owner_id", + "columns": [ + { + "expression": "owner_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_libraries_base_game_id_base_games_id_fk": { + "name": "game_libraries_base_game_id_base_games_id_fk", + "tableFrom": "game_libraries", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_libraries_owner_steam_id_steam_accounts_id_fk": { + "name": "game_libraries_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "game_libraries", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "game_libraries_base_game_id_owner_steam_id_pk": { + "name": "game_libraries_base_game_id_owner_steam_id_pk", + "columns": [ + "base_game_id", + "owner_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_member_steam_id": { + "name": "idx_member_steam_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_member_user_id": { + "name": "idx_member_user_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"members\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_steam_id_steam_accounts_id_fk": { + "name": "members_steam_id_steam_accounts_id_fk", + "tableFrom": "members", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "restrict" + } + }, + "compositePrimaryKeys": { + "members_id_team_id_pk": { + "name": "members_id_team_id_pk", + "columns": [ + "id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.steam_accounts": { + "name": "steam_accounts", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "steam_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "real_name": { + "name": "real_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "member_since": { + "name": "member_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_url": { + "name": "profile_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_hash": { + "name": "avatar_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "steam_accounts_user_id_users_id_fk": { + "name": "steam_accounts_user_id_users_id_fk", + "tableFrom": "steam_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "max_members": { + "name": "max_members", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_steam_id_steam_accounts_id_fk": { + "name": "teams_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "teams", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_team_invite_code": { + "name": "idx_team_invite_code", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "polar_customer_id": { + "name": "polar_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_user_email": { + "name": "idx_user_email", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.compatibility": { + "name": "compatibility", + "schema": "public", + "values": [ + "high", + "mid", + "low", + "unknown" + ] + }, + "public.controller_support": { + "name": "controller_support", + "schema": "public", + "values": [ + "full", + "partial", + "unknown" + ] + }, + "public.category_type": { + "name": "category_type", + "schema": "public", + "values": [ + "tag", + "genre", + "publisher", + "developer", + "categorie", + "franchise" + ] + }, + "public.image_type": { + "name": "image_type", + "schema": "public", + "values": [ + "heroArt", + "icon", + "logo", + "banner", + "poster", + "boxArt", + "screenshot", + "backdrop" + ] + }, + "public.member_role": { + "name": "member_role", + "schema": "public", + "values": [ + "child", + "adult" + ] + }, + "public.steam_status": { + "name": "steam_status", + "schema": "public", + "values": [ + "online", + "offline", + "dnd", + "playing" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/core/migrations/meta/0024_snapshot.json b/packages/core/migrations/meta/0024_snapshot.json new file mode 100644 index 00000000..e54f96df --- /dev/null +++ b/packages/core/migrations/meta/0024_snapshot.json @@ -0,0 +1,1166 @@ +{ + "id": "d35aa09b-5739-46a5-86f3-3050913dc2f7", + "prevId": "10e3e27a-30cc-4ef1-9966-a2490571942e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.base_games": { + "name": "base_games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "links": { + "name": "links", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_date": { + "name": "release_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "primary_genre": { + "name": "primary_genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "controller_support": { + "name": "controller_support", + "type": "controller_support", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "compatibility": { + "name": "compatibility", + "type": "compatibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "score": { + "name": "score", + "type": "numeric(2, 1)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_base_games_slug": { + "name": "idx_base_games_slug", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_categories_type": { + "name": "idx_categories_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "categories_slug_type_pk": { + "name": "categories_slug_type_pk", + "columns": [ + "slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends_list": { + "name": "friends_list", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "friend_steam_id": { + "name": "friend_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_friends_list_friend_steam_id": { + "name": "idx_friends_list_friend_steam_id", + "columns": [ + { + "expression": "friend_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_list_steam_id_steam_accounts_id_fk": { + "name": "friends_list_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "friends_list_friend_steam_id_steam_accounts_id_fk": { + "name": "friends_list_friend_steam_id_steam_accounts_id_fk", + "tableFrom": "friends_list", + "tableTo": "steam_accounts", + "columnsFrom": [ + "friend_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "friends_list_steam_id_friend_steam_id_pk": { + "name": "friends_list_steam_id_friend_steam_id_pk", + "columns": [ + "steam_id", + "friend_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.games": { + "name": "games", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "category_slug": { + "name": "category_slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "category_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_games_category_slug": { + "name": "idx_games_category_slug", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_type": { + "name": "idx_games_category_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_games_category_slug_type": { + "name": "idx_games_category_slug_type", + "columns": [ + { + "expression": "category_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "games_base_game_id_base_games_id_fk": { + "name": "games_base_game_id_base_games_id_fk", + "tableFrom": "games", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "games_categories_fkey": { + "name": "games_categories_fkey", + "tableFrom": "games", + "tableTo": "categories", + "columnsFrom": [ + "category_slug", + "type" + ], + "columnsTo": [ + "slug", + "type" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "games_base_game_id_category_slug_type_pk": { + "name": "games_base_game_id_category_slug_type_pk", + "columns": [ + "base_game_id", + "category_slug", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.images": { + "name": "images", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "image_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image_hash": { + "name": "image_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dimensions": { + "name": "dimensions", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "extracted_color": { + "name": "extracted_color", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_images_type": { + "name": "idx_images_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_images_game_id": { + "name": "idx_images_game_id", + "columns": [ + { + "expression": "base_game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "images_base_game_id_base_games_id_fk": { + "name": "images_base_game_id_base_games_id_fk", + "tableFrom": "images", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "images_image_hash_type_base_game_id_position_pk": { + "name": "images_image_hash_type_base_game_id_position_pk", + "columns": [ + "image_hash", + "type", + "base_game_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_libraries": { + "name": "game_libraries", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "base_game_id": { + "name": "base_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_played": { + "name": "last_played", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_playtime": { + "name": "total_playtime", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_game_libraries_owner_id": { + "name": "idx_game_libraries_owner_id", + "columns": [ + { + "expression": "owner_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_libraries_base_game_id_base_games_id_fk": { + "name": "game_libraries_base_game_id_base_games_id_fk", + "tableFrom": "game_libraries", + "tableTo": "base_games", + "columnsFrom": [ + "base_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_libraries_owner_steam_id_steam_accounts_id_fk": { + "name": "game_libraries_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "game_libraries", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "game_libraries_base_game_id_owner_steam_id_pk": { + "name": "game_libraries_base_game_id_owner_steam_id_pk", + "columns": [ + "base_game_id", + "owner_steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "char(30)", + "primaryKey": false, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_member_steam_id": { + "name": "idx_member_steam_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_member_user_id": { + "name": "idx_member_user_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"members\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_steam_id_steam_accounts_id_fk": { + "name": "members_steam_id_steam_accounts_id_fk", + "tableFrom": "members", + "tableTo": "steam_accounts", + "columnsFrom": [ + "steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "restrict" + } + }, + "compositePrimaryKeys": { + "members_id_team_id_pk": { + "name": "members_id_team_id_pk", + "columns": [ + "id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.steam_accounts": { + "name": "steam_accounts", + "schema": "", + "columns": { + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(30)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "steam_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "real_name": { + "name": "real_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "member_since": { + "name": "member_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_url": { + "name": "profile_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_hash": { + "name": "avatar_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "steam_accounts_user_id_users_id_fk": { + "name": "steam_accounts_user_id_users_id_fk", + "tableFrom": "steam_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "owner_steam_id": { + "name": "owner_steam_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "max_members": { + "name": "max_members", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_steam_id_steam_accounts_id_fk": { + "name": "teams_owner_steam_id_steam_accounts_id_fk", + "tableFrom": "teams", + "tableTo": "steam_accounts", + "columnsFrom": [ + "owner_steam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_team_invite_code": { + "name": "idx_team_invite_code", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(30)", + "primaryKey": true, + "notNull": true + }, + "time_created": { + "name": "time_created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "polar_customer_id": { + "name": "polar_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "idx_user_email": { + "name": "idx_user_email", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.compatibility": { + "name": "compatibility", + "schema": "public", + "values": [ + "high", + "mid", + "low", + "unknown" + ] + }, + "public.controller_support": { + "name": "controller_support", + "schema": "public", + "values": [ + "full", + "partial", + "unknown" + ] + }, + "public.category_type": { + "name": "category_type", + "schema": "public", + "values": [ + "tag", + "genre", + "publisher", + "developer", + "categorie", + "franchise" + ] + }, + "public.image_type": { + "name": "image_type", + "schema": "public", + "values": [ + "heroArt", + "icon", + "logo", + "banner", + "poster", + "boxArt", + "screenshot", + "backdrop" + ] + }, + "public.member_role": { + "name": "member_role", + "schema": "public", + "values": [ + "child", + "adult" + ] + }, + "public.steam_status": { + "name": "steam_status", + "schema": "public", + "values": [ + "online", + "offline", + "dnd", + "playing" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/core/migrations/meta/_journal.json b/packages/core/migrations/meta/_journal.json index 61d8e3e1..144907e2 100644 --- a/packages/core/migrations/meta/_journal.json +++ b/packages/core/migrations/meta/_journal.json @@ -141,6 +141,41 @@ "when": 1747202158003, "tag": "0019_charming_namorita", "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1747795508868, + "tag": "0020_vengeful_wallop", + "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1747975397543, + "tag": "0021_real_skreet", + "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1748099972605, + "tag": "0022_clean_living_lightning", + "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1748411845939, + "tag": "0023_flawless_steel_serpent", + "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1748414049463, + "tag": "0024_damp_cerise", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 26a92f7b..ab123ce2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,6 +17,7 @@ "@tsconfig/node20": "^20.1.4", "@types/pngjs": "^6.0.5", "@types/sanitize-html": "^2.16.0", + "@types/xml2js": "^0.4.14", "aws-iot-device-sdk-v2": "^1.21.1", "aws4fetch": "^1.0.20", "mqtt": "^5.10.3", @@ -42,6 +43,7 @@ "postgres": "^3.4.5", "sanitize-html": "^2.16.0", "sharp": "^0.34.1", - "steam-session": "*" + "steam-session": "*", + "xml2js": "^0.6.2" } } \ No newline at end of file diff --git a/packages/core/src/account/index.ts b/packages/core/src/account/index.ts index 4f542098..7a2921d5 100644 --- a/packages/core/src/account/index.ts +++ b/packages/core/src/account/index.ts @@ -1,6 +1,6 @@ import { z } from "zod" import { User } from "../user"; -import { Team } from "../team"; +import { Steam } from "../steam"; import { Actor } from "../actor"; import { Examples } from "../examples"; import { ErrorCodes, VisibleError } from "../error"; @@ -9,26 +9,26 @@ export namespace Account { export const Info = User.Info .extend({ - teams: Team.Info + profiles: Steam.Info .array() .openapi({ - description: "The teams that this user is part of", - example: [Examples.Team] + description: "The Steam accounts this user owns", + example: [Examples.SteamAccount] }) }) .openapi({ ref: "Account", description: "Represents an account's information stored on Nestri", - example: { ...Examples.User, teams: [Examples.Team] }, + example: { ...Examples.User, profiles: [Examples.SteamAccount] }, }); export type Info = z.infer; export const list = async (): Promise => { - const [userResult, teamsResult] = + const [userResult, steamResult] = await Promise.allSettled([ User.fromID(Actor.userID()), - Team.list() + Steam.list() ]) if (userResult.status === "rejected" || !userResult.value) @@ -40,7 +40,7 @@ export namespace Account { return { ...userResult.value, - teams: teamsResult.status === "rejected" ? [] : teamsResult.value + profiles: steamResult.status === "rejected" ? [] : steamResult.value } } diff --git a/packages/core/src/actor.ts b/packages/core/src/actor.ts index fa73048b..16720dde 100644 --- a/packages/core/src/actor.ts +++ b/packages/core/src/actor.ts @@ -11,11 +11,11 @@ export namespace Actor { email: string; }; } - - export interface System { - type: "system"; + + export interface Steam { + type: "steam"; properties: { - teamID: string; + steamID: string; }; } @@ -32,7 +32,6 @@ export namespace Actor { properties: { userID: string; steamID: string; - teamID: string; }; } @@ -41,7 +40,7 @@ export namespace Actor { properties: {}; } - export type Info = User | Public | Token | System | Machine; + export type Info = User | Public | Token | Machine | Steam; export const Context = createContext(); diff --git a/packages/core/src/base-game/base-game.sql.ts b/packages/core/src/base-game/base-game.sql.ts index 284a8b76..369e0f1c 100644 --- a/packages/core/src/base-game/base-game.sql.ts +++ b/packages/core/src/base-game/base-game.sql.ts @@ -3,15 +3,18 @@ import { timestamps, utc } from "../drizzle/types"; import { json, numeric, pgEnum, pgTable, text, unique, varchar } from "drizzle-orm/pg-core"; export const CompatibilityEnum = pgEnum("compatibility", ["high", "mid", "low", "unknown"]) -export const ControllerEnum = pgEnum("controller_support", ["full","partial", "unknown"]) +export const ControllerEnum = pgEnum("controller_support", ["full", "partial", "unknown"]) export const Size = z.object({ downloadSize: z.number().positive().int(), sizeOnDisk: z.number().positive().int() - }) + }); -export type Size = z.infer +export const Links = z.string().array(); + +export type Size = z.infer; +export type Links = z.infer; export const baseGamesTable = pgTable( "base_games", @@ -20,12 +23,13 @@ export const baseGamesTable = pgTable( id: varchar("id", { length: 255 }) .primaryKey() .notNull(), + links: json("links").$type(), slug: varchar("slug", { length: 255 }) .notNull(), name: text("name").notNull(), + description: text("description"), releaseDate: utc("release_date").notNull(), size: json("size").$type().notNull(), - description: text("description").notNull(), primaryGenre: text("primary_genre"), controllerSupport: ControllerEnum("controller_support").notNull(), compatibility: CompatibilityEnum("compatibility").notNull().default("unknown"), diff --git a/packages/core/src/base-game/index.ts b/packages/core/src/base-game/index.ts index 19d70b88..989b2880 100644 --- a/packages/core/src/base-game/index.ts +++ b/packages/core/src/base-game/index.ts @@ -1,13 +1,12 @@ import { z } from "zod"; import { fn } from "../utils"; -import { Resource } from "sst"; -import { bus } from "sst/aws/bus"; import { Common } from "../common"; import { Examples } from "../examples"; import { createEvent } from "../event"; import { eq, isNull, and } from "drizzle-orm"; -import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; -import { CompatibilityEnum, baseGamesTable, Size, ControllerEnum } from "./base-game.sql"; +import { ImageTypeEnum } from "../images/images.sql"; +import { createTransaction, useTransaction } from "../drizzle/transaction"; +import { CompatibilityEnum, baseGamesTable, Size, ControllerEnum, Links } from "./base-game.sql"; export namespace BaseGame { export const Info = z.object({ @@ -31,7 +30,7 @@ export namespace BaseGame { description: "The initial public release date of the game on Steam", example: Examples.BaseGame.releaseDate }), - description: z.string().openapi({ + description: z.string().nullable().openapi({ description: "A comprehensive overview of the game, including its features, storyline, and gameplay elements", example: Examples.BaseGame.description }), @@ -39,6 +38,12 @@ export namespace BaseGame { description: "The aggregate user review score on Steam, represented as a percentage of positive reviews", example: Examples.BaseGame.score }), + links: Links + .nullable() + .openapi({ + description: "The social links of this game", + example: Examples.BaseGame.links + }), primaryGenre: z.string().nullable().openapi({ description: "The main category or genre that best represents the game's content and gameplay style", example: Examples.BaseGame.primaryGenre @@ -50,7 +55,7 @@ export namespace BaseGame { compatibility: z.enum(CompatibilityEnum.enumValues).openapi({ description: "Steam Deck/Proton compatibility rating indicating how well the game runs on Linux systems", example: Examples.BaseGame.compatibility - }) + }), }).openapi({ ref: "BaseGame", description: "Detailed information about a game available in the Nestri library, including technical specifications and metadata", @@ -61,9 +66,27 @@ export namespace BaseGame { export const Events = { New: createEvent( - "new_game.added", + "new_image.save", z.object({ appID: Info.shape.id, + url: z.string().url(), + type: z.enum(ImageTypeEnum.enumValues) + }), + ), + NewBoxArt: createEvent( + "new_box_art_image.save", + z.object({ + appID: Info.shape.id, + logoUrl: z.string().url(), + backgroundUrl: z.string().url(), + }), + ), + NewHeroArt: createEvent( + "new_hero_art_image.save", + z.object({ + appID: Info.shape.id, + backdropUrl: z.string().url(), + screenshots: z.string().url().array(), }), ), }; @@ -72,6 +95,21 @@ export namespace BaseGame { Info, (input) => createTransaction(async (tx) => { + const result = await tx + .select() + .from(baseGamesTable) + .where( + and( + eq(baseGamesTable.id, input.id), + isNull(baseGamesTable.timeDeleted) + ) + ) + .limit(1) + .execute() + .then(rows => rows.at(0)) + + if (result) return result.id + await tx .insert(baseGamesTable) .values(input) @@ -82,10 +120,6 @@ export namespace BaseGame { } }) - await afterTx(async () => { - await bus.publish(Resource.Bus, Events.New, { appID: input.id }) - }) - return input.id }) ) @@ -116,6 +150,7 @@ export namespace BaseGame { name: input.name, slug: input.slug, size: input.size, + links: input.links, score: input.score, description: input.description, releaseDate: input.releaseDate, diff --git a/packages/core/src/categories/categories.sql.ts b/packages/core/src/categories/categories.sql.ts index df85ce21..cf74cbca 100644 --- a/packages/core/src/categories/categories.sql.ts +++ b/packages/core/src/categories/categories.sql.ts @@ -1,7 +1,8 @@ import { timestamps } from "../drizzle/types"; import { index, pgEnum, pgTable, primaryKey, text, varchar } from "drizzle-orm/pg-core"; -export const CategoryTypeEnum = pgEnum("category_type", ["tag", "genre", "publisher", "developer"]) +// Intentional grammatical error on category +export const CategoryTypeEnum = pgEnum("category_type", ["tag", "genre", "publisher", "developer", "categorie", "franchise"]) export const categoriesTable = pgTable( "categories", diff --git a/packages/core/src/categories/index.ts b/packages/core/src/categories/index.ts index 4ea20424..ff077f85 100644 --- a/packages/core/src/categories/index.ts +++ b/packages/core/src/categories/index.ts @@ -1,10 +1,10 @@ import { z } from "zod"; import { fn } from "../utils"; import { Examples } from "../examples"; +import { eq, isNull, and } from "drizzle-orm"; import { createSelectSchema } from "drizzle-zod"; import { categoriesTable } from "./categories.sql"; import { createTransaction, useTransaction } from "../drizzle/transaction"; -import { eq, isNull, and } from "drizzle-orm"; export namespace Categories { @@ -36,7 +36,16 @@ export namespace Categories { genres: Category.array().openapi({ description: "Primary classification categories that define the game's style and type of gameplay", example: Examples.Categories.genres - }) + }), + categories: Category.array().openapi({ + description: "Primary classification categories that define the game's categorisation on Steam", + example: Examples.Categories.genres + }), + franchises: Category.array().openapi({ + description: "The franchise this game belongs belongs to on Steam", + example: Examples.Categories.genres + }), + }).openapi({ ref: "Categories", description: "A comprehensive categorization system for games, including publishing details, development credits, and content classification", @@ -111,7 +120,9 @@ export namespace Categories { tags: [], genres: [], publishers: [], - developers: [] + developers: [], + categories: [], + franchises: [] }) } } \ No newline at end of file diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 58edfe6e..a63c2c07 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -1,159 +1,172 @@ import type { + Shot, AppInfo, - GameTagsResponse, - SteamApiResponse, - GameDetailsResponse, - SteamAppDataResponse, ImageInfo, ImageType, - Shot + SteamAccount, + GameTagsResponse, + GameDetailsResponse, + SteamAppDataResponse, + SteamOwnedGamesResponse, + SteamPlayerBansResponse, + SteamFriendsListResponse, + SteamPlayerSummaryResponse, + SteamStoreResponse, } from "./types"; import { z } from "zod"; -import pLimit from 'p-limit'; -import SteamID from "steamid"; import { fn } from "../utils"; +import { Resource } from "sst"; +import { Steam } from "./steam"; import { Utils } from "./utils"; -import SteamCommunity from "steamcommunity"; -import { Credentials } from "../credentials"; -import type CSteamUser from "steamcommunity/classes/CSteamUser"; - -const requestLimit = pLimit(10); // max concurrent requests +import { ImageTypeEnum } from "../images/images.sql"; export namespace Client { export const getUserLibrary = fn( - Credentials.Info.shape.accessToken, - async (accessToken) => - await Utils.fetchApi(`https://api.steampowered.com/IFamilyGroupsService/GetSharedLibraryApps/v1/?access_token=${accessToken}&family_groupid=0&include_excluded=true&include_free=true&include_non_games=false&include_own=true`) + z.string(), + async (steamID) => + await Utils.fetchApi(`https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=${Resource.SteamApiKey.value}&steamid=${steamID}&include_appinfo=1&format=json&include_played_free_games=1&skip_unvetted_apps=0`) ) export const getFriendsList = fn( - Credentials.Info.shape.cookies, - async (cookies): Promise => { - const community = new SteamCommunity(); - community.setCookies(cookies); - - const allFriends = await new Promise>((resolve, reject) => { - community.getFriendsList((err, friends) => { - if (err) { - return reject(new Error(`Could not get friends list: ${err.message}`)); - } - resolve(friends); - }); - }); - - const friendIds = Object.keys(allFriends); - - const userPromises: Promise[] = friendIds.map(id => - requestLimit(() => new Promise((resolve, reject) => { - const sid = new SteamID(id); - community.getSteamUser(sid, (err, user) => { - if (err) { - return reject(new Error(`Could not get steam user info for ${id}: ${err.message}`)); - } - resolve(user); - }); - })) - ); - - const settled = await Promise.allSettled(userPromises) - - settled - .filter(r => r.status === "rejected") - .forEach(r => console.warn("[getFriendsList] failed:", (r as PromiseRejectedResult).reason)); - - return settled.filter(s => s.status === "fulfilled").map(r => (r as PromiseFulfilledResult).value); - } + z.string(), + async (steamID) => + await Utils.fetchApi(`https://api.steampowered.com/ISteamUser/GetFriendList/v0001/?key=${Resource.SteamApiKey.value}&steamid=${steamID}&relationship=friend`) ); export const getUserInfo = fn( - Credentials.Info.pick({ cookies: true, steamID: true }), - async (input) => - new Promise((resolve, reject) => { - const community = new SteamCommunity() - community.setCookies(input.cookies); - const steamID = new SteamID(input.steamID); - community.getSteamUser(steamID, async (err, user) => { - if (err) { - reject(`Could not get steam user info: ${err.message}`) + z.string().array(), + async (steamIDs) => { + const [userInfo, banInfo, profileInfo] = await Promise.all([ + Utils.fetchApi(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${Resource.SteamApiKey.value}&steamids=${steamIDs.join(",")}`), + Utils.fetchApi(`https://api.steampowered.com/ISteamUser/GetPlayerBans/v1/?key=${Resource.SteamApiKey.value}&steamids=${steamIDs.join(",")}`), + Utils.fetchProfilesInfo(steamIDs) + ]) + + // Create a map of bans by steamID for fast lookup + const bansBySteamID = new Map( + banInfo.players.map((b) => [b.SteamId, b]) + ); + + // Map userInfo.players to your desired output using Promise.allSettled + // to prevent one error from closing down the whole pipeline + const steamAccounts = await Promise.allSettled( + userInfo.response.players.map(async (player) => { + const ban = bansBySteamID.get(player.steamid); + const info = profileInfo.get(player.steamid); + + if (!info) { + throw new Error(`[userInfo] profile info missing for ${player.steamid}`) + } + + if ('error' in info) { + throw new Error(`error handling profile info for: ${player.steamid}:${info.error}`) } else { - resolve(user) + return { + id: player.steamid, + name: player.personaname, + realName: player.realname ?? null, + steamMemberSince: new Date(player.timecreated * 1000), + avatarHash: player.avatarhash, + limitations: { + isLimited: info.isLimited, + privacyState: info.privacyState, + isVacBanned: ban?.VACBanned ?? false, + tradeBanState: ban?.EconomyBan ?? "none", + visibilityState: player.communityvisibilitystate, + }, + lastSyncedAt: new Date(), + profileUrl: player.profileurl, + }; } }) - }) as Promise - ) + ); + + steamAccounts + .filter(result => result.status === 'rejected') + .forEach(result => console.warn('[userInfo] failed:', (result as PromiseRejectedResult).reason)) + + return steamAccounts.filter(result => result.status === "fulfilled").map(result => (result as PromiseFulfilledResult).value) + }) export const getAppInfo = fn( z.string(), async (appid) => { - const [infoData, tagsData, details] = await Promise.all([ - Utils.fetchApi(`https://api.steamcmd.net/v1/info/${appid}`), - Utils.fetchApi("https://store.steampowered.com/actions/ajaxgetstoretags"), - Utils.fetchApi( - `https://store.steampowered.com/apphover/${appid}?full=1&review_score_preference=1&pagev6=true&json=1` - ), - ]); + try { + const info = await Promise.all([ + Utils.fetchApi(`https://api.steamcmd.net/v1/info/${appid}`), + Utils.fetchApi(`https://api.steampowered.com/IStoreBrowseService/GetItems/v1/?key=${Resource.SteamApiKey.value}&input_json={"ids":[{"appid":"${appid}"}],"context":{"language":"english","country_code":"US","steam_realm":"1"},"data_request":{"include_assets":true,"include_release":true,"include_platforms":true,"include_all_purchase_options":true,"include_screenshots":true,"include_trailers":true,"include_ratings":true,"include_tag_count":"40","include_reviews":true,"include_basic_info":true,"include_supported_languages":true,"include_full_description":true,"include_included_items":true,"include_assets_without_overrides":true,"apply_user_filters":true,"include_links":true}}`), + ]); - const tags = tagsData.tags; - const game = infoData.data[appid]; - // Guard against an empty string - When there are no genres, Steam returns an empty string - const genres = details.strGenres ? Utils.parseGenres(details.strGenres) : []; + const cmd = info[0].data[appid] + const store = info[1].response.store_items[0] - const controllerTag = game.common.controller_support ? - Utils.createTag(`${Utils.capitalise(game.common.controller_support)} Controller Support`) : - Utils.createTag(`Unknown Controller Support`) + if (!cmd) { + throw new Error(`App data not found for appid: ${appid}`) + } - const compatibilityTag = Utils.createTag(`${Utils.capitalise(Utils.compatibilityType(game.common.steam_deck_compatibility?.category))} Compatibility`) + if (!store || store.success !== 1) { + throw new Error(`Could not get store information or appid: ${appid}`) + } - const controller = (game.common.controller_support === "partial" || game.common.controller_support === "full") ? game.common.controller_support : "unknown"; - const appInfo: AppInfo = { - genres, - gameid: game.appid, - name: game.common.name.trim(), - size: Utils.getPublicDepotSizes(game.depots!), - slug: Utils.createSlug(game.common.name.trim()), - description: Utils.cleanDescription(details.strDescription), - controllerSupport: controller, - releaseDate: new Date(Number(game.common.steam_release_date) * 1000), - primaryGenre: (!!game?.common.genres && !!details.strGenres) ? Utils.getPrimaryGenre( - genres, - game.common.genres!, - game.common.primary_genre! - ) : null, - developers: game.common.associations ? - Array.from( - Utils.getAssociationsByTypeWithSlug( - game.common.associations!, - "developer" - ) - ) : [], - publishers: game.common.associations ? - Array.from( - Utils.getAssociationsByTypeWithSlug( - game.common.associations!, - "publisher" - ) - ) : [], - compatibility: Utils.compatibilityType(game.common.steam_deck_compatibility?.category), - tags: [ - ...(game?.common.store_tags ? - Utils.mapGameTags( - tags, - game.common.store_tags!, - ) : []), - controllerTag, - compatibilityTag - ], - score: Utils.getRating( - details.ReviewSummary.cRecommendationsPositive, - details.ReviewSummary.cRecommendationsNegative - ), - }; + const tags = store.tagids + .map(id => Steam.tags[id.toString() as keyof typeof Steam.tags]) + .filter((name): name is string => typeof name === 'string') - return appInfo + const publishers = store.basic_info.publishers + .map(i => i.name) + + const developers = store.basic_info.developers + .map(i => i.name) + + const franchises = store.basic_info.franchises + ?.map(i => i.name) + + const genres = cmd?.common.genres && + Object.keys(cmd?.common.genres) + .map(id => Steam.genres[id.toString() as keyof typeof Steam.genres]) + .filter((name): name is string => typeof name === 'string') + + const categories = [ + ...(store.categories?.controller_categoryids?.map(i => Steam.categories[i.toString() as keyof typeof Steam.categories]) ?? []), + ...(store.categories?.supported_player_categoryids?.map(i => Steam.categories[i.toString() as keyof typeof Steam.categories]) ?? []) + ].filter((name): name is string => typeof name === 'string') + + const assetUrls = Utils.getAssetUrls(cmd?.common.library_assets_full, appid, cmd?.common.header_image.english); + + const screenshots = store.screenshots.all_ages_screenshots?.map(i => `https://shared.cloudflare.steamstatic.com/store_item_assets/${i.filename}`) ?? []; + + const icon = `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${appid}/${cmd?.common.icon}.jpg`; + + const data: AppInfo = { + id: appid, + name: cmd?.common.name.trim(), + tags: Utils.createType(tags, "tag"), + images: { screenshots, icon, ...assetUrls }, + size: Utils.getPublicDepotSizes(cmd?.depots!), + slug: Utils.createSlug(cmd?.common.name.trim()), + publishers: Utils.createType(publishers, "publisher"), + developers: Utils.createType(developers, "developer"), + categories: Utils.createType(categories, "categorie"), + links: store.links ? store.links.map(i => i.url) : null, + genres: genres ? Utils.createType(genres, "genre") : [], + franchises: franchises ? Utils.createType(franchises, "franchise") : [], + description: store.basic_info.short_description ? Utils.cleanDescription(store.basic_info.short_description) : null, + controllerSupport: cmd?.common.controller_support ?? "unknown" as any, + releaseDate: new Date(Number(cmd?.common.steam_release_date) * 1000), + primaryGenre: !!cmd?.common.primary_genre && Steam.genres[cmd?.common.primary_genre as keyof typeof Steam.genres] ? Steam.genres[cmd?.common.primary_genre as keyof typeof Steam.genres] : null, + compatibility: store?.platforms.steam_os_compat_category ? Utils.compatibilityType(store?.platforms.steam_os_compat_category.toString() as any).toLowerCase() : "unknown" as any, + score: Utils.estimateRatingFromSummary(store.reviews.summary_filtered.review_count, store.reviews.summary_filtered.percent_positive) + } + + return data + } catch (err) { + console.log(`Error handling: ${appid}`) + throw err + } } ) - export const getImages = fn( + export const getImageUrls = fn( z.string(), async (appid) => { const [appData, details] = await Promise.all([ @@ -167,18 +180,49 @@ export namespace Client { if (!game) throw new Error('Game info missing'); // 2. Prepare URLs - const screenshotUrls = Utils.getScreenshotUrls(details.rgScreenshots || []); + const screenshots = Utils.getScreenshotUrls(details.rgScreenshots || []); const assetUrls = Utils.getAssetUrls(game.library_assets_full, appid, game.header_image.english); - const iconUrl = `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${appid}/${game.icon}.jpg`; + const icon = `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${appid}/${game.icon}.jpg`; - //2.5 Get the backdrop buffer and use it to get the best screenshot - const baselineBuffer = await Utils.fetchBuffer(assetUrls.backdrop); + return { screenshots, icon, ...assetUrls } + } + ) - // 3. Download screenshot buffers in parallel + export const getImageInfo = fn( + z.object({ + type: z.enum(ImageTypeEnum.enumValues), + url: z.string() + }), + async (input) => + Utils.fetchBuffer(input.url) + .then(buf => Utils.getImageMetadata(buf)) + .then(meta => ({ ...meta, position: 0, sourceUrl: input.url, type: input.type } as ImageInfo)) + ) + + export const createBoxArt = fn( + z.object({ + backgroundUrl: z.string(), + logoUrl: z.string(), + }), + async (input) => + Utils.createBoxArtBuffer(input.logoUrl, input.backgroundUrl) + .then(buf => Utils.getImageMetadata(buf)) + .then(meta => ({ ...meta, position: 0, sourceUrl: null, type: 'boxArt' as const }) as ImageInfo) + ) + + export const createHeroArt = fn( + z.object({ + screenshots: z.string().array(), + backdropUrl: z.string() + }), + async (input) => { + // Download screenshot buffers in parallel const shots: Shot[] = await Promise.all( - screenshotUrls.map(async url => ({ url, buffer: await Utils.fetchBuffer(url) })) + input.screenshots.map(async url => ({ url, buffer: await Utils.fetchBuffer(url) })) ); + const baselineBuffer = await Utils.fetchBuffer(input.backdropUrl); + // 4. Score screenshots (or pick single) const scores = shots.length === 1 @@ -204,37 +248,69 @@ export namespace Client { ); } - // 5b. Asset images - for (const [type, url] of Object.entries({ ...assetUrls, icon: iconUrl })) { - if (!url || type === "backdrop") continue; - tasks.push( - Utils.fetchBuffer(url) - .then(buf => Utils.getImageMetadata(buf)) - .then(meta => ({ ...meta, position: 0, sourceUrl: url, type: type as ImageType } as ImageInfo)) - ); - } - - // 5c. Backdrop - tasks.push( - Utils.getImageMetadata(baselineBuffer) - .then(meta => ({ ...meta, position: 0, sourceUrl: assetUrls.backdrop, type: "backdrop" as const } as ImageInfo)) - ) - - // 5d. Box art - tasks.push( - Utils.createBoxArtBuffer(game.library_assets_full, appid) - .then(buf => Utils.getImageMetadata(buf)) - .then(meta => ({ ...meta, position: 0, sourceUrl: null, type: 'boxArt' as const }) as ImageInfo) - ); - - const settled = await Promise.allSettled(tasks) + const settled = await Promise.allSettled(tasks); settled .filter(r => r.status === "rejected") - .forEach(r => console.warn("[getImages] failed:", (r as PromiseRejectedResult).reason)); + .forEach(r => console.warn("[getHeroArt] failed:", (r as PromiseRejectedResult).reason)); - // 6. Await all and return + // Await all and return return settled.filter(s => s.status === "fulfilled").map(r => (r as PromiseFulfilledResult).value) } ) + + /** + * Verifies a Steam OpenID response by sending a request back to Steam + * with mode=check_authentication + */ + export async function verifyOpenIDResponse(params: URLSearchParams): Promise { + try { + // Create a new URLSearchParams with all the original parameters + const verificationParams = new URLSearchParams(); + + // Copy all parameters from the original request + for (const [key, value] of params.entries()) { + verificationParams.append(key, value); + } + + // Change mode to check_authentication for verification + verificationParams.set('openid.mode', 'check_authentication'); + + // Send verification request to Steam + const verificationResponse = await fetch('https://steamcommunity.com/openid/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: verificationParams.toString() + }); + + const responseText = await verificationResponse.text(); + + // Check if verification was successful + if (!responseText.includes('is_valid:true')) { + console.error('OpenID verification failed: Invalid response from Steam', responseText); + return null; + } + + // Extract steamID from the claimed_id + const claimedId = params.get('openid.claimed_id'); + if (!claimedId) { + console.error('OpenID verification failed: Missing claimed_id'); + return null; + } + + // Extract the Steam ID from the claimed_id + const steamID = claimedId.split('/').pop(); + if (!steamID || !/^\d+$/.test(steamID)) { + console.error('OpenID verification failed: Invalid steamID format', steamID); + return null; + } + + return steamID; + } catch (error) { + console.error('OpenID verification error:', error); + return null; + } + } } \ No newline at end of file diff --git a/packages/core/src/client/steam.ts b/packages/core/src/client/steam.ts new file mode 100644 index 00000000..ffaf321f --- /dev/null +++ b/packages/core/src/client/steam.ts @@ -0,0 +1,544 @@ +export namespace Steam { + //Source: https://github.com/woctezuma/steam-api/blob/master/data/genres.json + export const genres = { + "1": "Action", + "2": "Strategy", + "3": "RPG", + "4": "Casual", + "9": "Racing", + "18": "Sports", + "23": "Indie", + "25": "Adventure", + "28": "Simulation", + "29": "Massively Multiplayer", + "37": "Free to Play", + "50": "Accounting", + "51": "Animation & Modeling", + "52": "Audio Production", + "53": "Design & Illustration", + "54": "Education", + "55": "Photo Editing", + "56": "Software Training", + "57": "Utilities", + "58": "Video Production", + "59": "Web Publishing", + "60": "Game Development", + "70": "Early Access", + "71": "Sexual Content", + "72": "Nudity", + "73": "Violent", + "74": "Gore", + "80": "Movie", + "81": "Documentary", + "82": "Episodic", + "83": "Short", + "84": "Tutorial", + "85": "360 Video" + } + + //Source: https://github.com/woctezuma/steam-api/blob/master/data/categories.json + export const categories = { + "1": "Multi-player", + "2": "Single-player", + "6": "Mods (require HL2)", + "7": "Mods (require HL1)", + "8": "Valve Anti-Cheat enabled", + "9": "Co-op", + "10": "Demos", + "12": "HDR available", + "13": "Captions available", + "14": "Commentary available", + "15": "Stats", + "16": "Includes Source SDK", + "17": "Includes level editor", + "18": "Partial Controller Support", + "19": "Mods", + "20": "MMO", + "21": "Downloadable Content", + "22": "Steam Achievements", + "23": "Steam Cloud", + "24": "Shared/Split Screen", + "25": "Steam Leaderboards", + "27": "Cross-Platform Multiplayer", + "28": "Full controller support", + "29": "Steam Trading Cards", + "30": "Steam Workshop", + "31": "VR Support", + "32": "Steam Turn Notifications", + "33": "Native Steam Controller", + "35": "In-App Purchases", + "36": "Online PvP", + "37": "Shared/Split Screen PvP", + "38": "Online Co-op", + "39": "Shared/Split Screen Co-op", + "40": "SteamVR Collectibles", + "41": "Remote Play on Phone", + "42": "Remote Play on Tablet", + "43": "Remote Play on TV", + "44": "Remote Play Together", + "45": "Cloud Gaming", + "46": "Cloud Gaming (NVIDIA)", + "47": "LAN PvP", + "48": "LAN Co-op", + "49": "PvP", + "50": "Additional High-Quality Audio", + "51": "Steam Workshop", + "52": "Tracked Controller Support", + "53": "VR Supported", + "54": "VR Only" + } + + // Source: https://files.catbox.moe/96bty7.json + export const tags = { + "9": "Strategy", + "19": "Action", + "21": "Adventure", + "84": "Design & Illustration", + "87": "Utilities", + "113": "Free to Play", + "122": "RPG", + "128": "Massively Multiplayer", + "492": "Indie", + "493": "Early Access", + "597": "Casual", + "599": "Simulation", + "699": "Racing", + "701": "Sports", + "784": "Video Production", + "809": "Photo Editing", + "872": "Animation & Modeling", + "1027": "Audio Production", + "1036": "Education", + "1038": "Web Publishing", + "1445": "Software Training", + "1616": "Trains", + "1621": "Music", + "1625": "Platformer", + "1628": "Metroidvania", + "1638": "Dog", + "1643": "Building", + "1644": "Driving", + "1645": "Tower Defense", + "1646": "Hack and Slash", + "1647": "Western", + "1649": "GameMaker", + "1651": "Satire", + "1654": "Relaxing", + "1659": "Zombies", + "1662": "Survival", + "1663": "FPS", + "1664": "Puzzle", + "1665": "Match 3", + "1666": "Card Game", + "1667": "Horror", + "1669": "Moddable", + "1670": "4X", + "1671": "Superhero", + "1673": "Aliens", + "1674": "Typing", + "1676": "RTS", + "1677": "Turn-Based", + "1678": "War", + "1680": "Heist", + "1681": "Pirates", + "1684": "Fantasy", + "1685": "Co-op", + "1687": "Stealth", + "1688": "Ninja", + "1693": "Classic", + "1695": "Open World", + "1697": "Third Person", + "1698": "Point & Click", + "1702": "Crafting", + "1708": "Tactical", + "1710": "Surreal", + "1714": "Psychedelic", + "1716": "Roguelike", + "1717": "Hex Grid", + "1718": "MOBA", + "1719": "Comedy", + "1720": "Dungeon Crawler", + "1721": "Psychological Horror", + "1723": "Action RTS", + "1730": "Sokoban", + "1732": "Voxel", + "1733": "Unforgiving", + "1734": "Fast-Paced", + "1736": "LEGO", + "1738": "Hidden Object", + "1741": "Turn-Based Strategy", + "1742": "Story Rich", + "1743": "Fighting", + "1746": "Basketball", + "1751": "Comic Book", + "1752": "Rhythm", + "1753": "Skateboarding", + "1754": "MMORPG", + "1755": "Space", + "1756": "Great Soundtrack", + "1759": "Perma Death", + "1770": "Board Game", + "1773": "Arcade", + "1774": "Shooter", + "1775": "PvP", + "1777": "Steampunk", + "3796": "Based On A Novel", + "3798": "Side Scroller", + "3799": "Visual Novel", + "3810": "Sandbox", + "3813": "Real Time Tactics", + "3814": "Third-Person Shooter", + "3834": "Exploration", + "3835": "Post-apocalyptic", + "3839": "First-Person", + "3841": "Local Co-Op", + "3843": "Online Co-Op", + "3854": "Lore-Rich", + "3859": "Multiplayer", + "3871": "2D", + "3877": "Precision Platformer", + "3878": "Competitive", + "3916": "Old School", + "3920": "Cooking", + "3934": "Immersive", + "3942": "Sci-fi", + "3952": "Gothic", + "3955": "Character Action Game", + "3959": "Roguelite", + "3964": "Pixel Graphics", + "3965": "Epic", + "3968": "Physics", + "3978": "Survival Horror", + "3987": "Historical", + "3993": "Combat", + "4004": "Retro", + "4018": "Vampire", + "4026": "Difficult", + "4036": "Parkour", + "4046": "Dragons", + "4057": "Magic", + "4064": "Thriller", + "4085": "Anime", + "4094": "Minimalist", + "4102": "Combat Racing", + "4106": "Action-Adventure", + "4115": "Cyberpunk", + "4136": "Funny", + "4137": "Transhumanism", + "4145": "Cinematic", + "4150": "World War II", + "4155": "Class-Based", + "4158": "Beat 'em up", + "4161": "Real-Time", + "4166": "Atmospheric", + "4168": "Military", + "4172": "Medieval", + "4175": "Realistic", + "4182": "Singleplayer", + "4184": "Chess", + "4190": "Addictive", + "4191": "3D", + "4195": "Cartoony", + "4202": "Trading", + "4231": "Action RPG", + "4234": "Short", + "4236": "Loot", + "4242": "Episodic", + "4252": "Stylized", + "4255": "Shoot 'Em Up", + "4291": "Spaceships", + "4295": "Futuristic", + "4305": "Colorful", + "4325": "Turn-Based Combat", + "4328": "City Builder", + "4342": "Dark", + "4345": "Gore", + "4364": "Grand Strategy", + "4376": "Assassin", + "4400": "Abstract", + "4434": "JRPG", + "4474": "CRPG", + "4486": "Choose Your Own Adventure", + "4508": "Co-op Campaign", + "4520": "Farming", + "4559": "Quick-Time Events", + "4562": "Cartoon", + "4598": "Alternate History", + "4604": "Dark Fantasy", + "4608": "Swordplay", + "4637": "Top-Down Shooter", + "4667": "Violent", + "4684": "Wargame", + "4695": "Economy", + "4700": "Movie", + "4711": "Replay Value", + "4726": "Cute", + "4736": "2D Fighter", + "4747": "Character Customization", + "4754": "Politics", + "4758": "Twin Stick Shooter", + "4777": "Spectacle fighter", + "4791": "Top-Down", + "4821": "Mechs", + "4835": "6DOF", + "4840": "4 Player Local", + "4845": "Capitalism", + "4853": "Political", + "4878": "Parody", + "4885": "Bullet Hell", + "4947": "Romance", + "4975": "2.5D", + "4994": "Naval Combat", + "5030": "Dystopian", + "5055": "eSports", + "5094": "Narration", + "5125": "Procedural Generation", + "5153": "Kickstarter", + "5154": "Score Attack", + "5160": "Dinosaurs", + "5179": "Cold War", + "5186": "Psychological", + "5228": "Blood", + "5230": "Sequel", + "5300": "God Game", + "5310": "Games Workshop", + "5348": "Mod", + "5350": "Family Friendly", + "5363": "Destruction", + "5372": "Conspiracy", + "5379": "2D Platformer", + "5382": "World War I", + "5390": "Time Attack", + "5395": "3D Platformer", + "5407": "Benchmark", + "5411": "Beautiful", + "5432": "Programming", + "5502": "Hacking", + "5537": "Puzzle Platformer", + "5547": "Arena Shooter", + "5577": "RPGMaker", + "5608": "Emotional", + "5611": "Mature", + "5613": "Detective", + "5652": "Collectathon", + "5673": "Modern", + "5708": "Remake", + "5711": "Team-Based", + "5716": "Mystery", + "5727": "Baseball", + "5752": "Robots", + "5765": "Gun Customization", + "5794": "Science", + "5796": "Bullet Time", + "5851": "Isometric", + "5900": "Walking Simulator", + "5914": "Tennis", + "5923": "Dark Humor", + "5941": "Reboot", + "5981": "Mining", + "5984": "Drama", + "6041": "Horses", + "6052": "Noir", + "6129": "Logic", + "6214": "Birds", + "6276": "Inventory Management", + "6310": "Diplomacy", + "6378": "Crime", + "6426": "Choices Matter", + "6506": "3D Fighter", + "6621": "Pinball", + "6625": "Time Manipulation", + "6650": "Nudity", + "6691": "1990's", + "6702": "Mars", + "6730": "PvE", + "6815": "Hand-drawn", + "6869": "Nonlinear", + "6910": "Naval", + "6915": "Martial Arts", + "6948": "Rome", + "6971": "Multiple Endings", + "7038": "Golf", + "7107": "Real-Time with Pause", + "7108": "Party", + "7113": "Crowdfunded", + "7178": "Party Game", + "7208": "Female Protagonist", + "7250": "Linear", + "7309": "Skiing", + "7328": "Bowling", + "7332": "Base Building", + "7368": "Local Multiplayer", + "7423": "Sniper", + "7432": "Lovecraftian", + "7478": "Illuminati", + "7481": "Controller", + "7556": "Dice", + "7569": "Grid-Based Movement", + "7622": "Offroad", + "7702": "Narrative", + "7743": "1980s", + "7782": "Cult Classic", + "7918": "Dwarf", + "7926": "Artificial Intelligence", + "7948": "Soundtrack", + "8013": "Software", + "8075": "TrackIR", + "8093": "Minigames", + "8122": "Level Editor", + "8253": "Music-Based Procedural Generation", + "8369": "Investigation", + "8461": "Well-Written", + "8666": "Runner", + "8945": "Resource Management", + "9130": "Hentai", + "9157": "Underwater", + "9204": "Immersive Sim", + "9271": "Trading Card Game", + "9541": "Demons", + "9551": "Dating Sim", + "9564": "Hunting", + "9592": "Dynamic Narration", + "9803": "Snow", + "9994": "Experience", + "10235": "Life Sim", + "10383": "Transportation", + "10397": "Memes", + "10437": "Trivia", + "10679": "Time Travel", + "10695": "Party-Based RPG", + "10808": "Supernatural", + "10816": "Split Screen", + "11014": "Interactive Fiction", + "11095": "Boss Rush", + "11104": "Vehicular Combat", + "11123": "Mouse only", + "11333": "Villain Protagonist", + "11634": "Vikings", + "12057": "Tutorial", + "12095": "Sexual Content", + "12190": "Boxing", + "12286": "Warhammer 40K", + "12472": "Management", + "13070": "Solitaire", + "13190": "America", + "13276": "Tanks", + "13382": "Archery", + "13577": "Sailing", + "13782": "Experimental", + "13906": "Game Development", + "14139": "Turn-Based Tactics", + "14153": "Dungeons & Dragons", + "14720": "Nostalgia", + "14906": "Intentionally Awkward Controls", + "15045": "Flight", + "15172": "Conversation", + "15277": "Philosophical", + "15339": "Documentary", + "15564": "Fishing", + "15868": "Motocross", + "15954": "Silent Protagonist", + "16094": "Mythology", + "16250": "Gambling", + "16598": "Space Sim", + "16689": "Time Management", + "17015": "Werewolves", + "17305": "Strategy RPG", + "17337": "Lemmings", + "17389": "Tabletop", + "17770": "Asynchronous Multiplayer", + "17894": "Cats", + "17927": "Pool", + "18594": "FMV", + "19568": "Cycling", + "19780": "Submarine", + "19995": "Dark Comedy", + "21006": "Underground", + "21491": "Demo Available", + "21725": "Tactical RPG", + "21978": "VR", + "22602": "Agriculture", + "22955": "Mini Golf", + "24003": "Word Game", + "24904": "NSFW", + "25085": "Touch-Friendly", + "26921": "Political Sim", + "27758": "Voice Control", + "28444": "Snowboarding", + "29363": "3D Vision", + "29482": "Souls-like", + "29855": "Ambient", + "30358": "Nature", + "30927": "Fox", + "31275": "Text-Based", + "31579": "Otome", + "32322": "Deckbuilding", + "33572": "Mahjong", + "35079": "Job Simulator", + "42089": "Jump Scare", + "42329": "Coding", + "42804": "Action Roguelike", + "44868": "LGBTQ+", + "47827": "Wrestling", + "49213": "Rugby", + "51306": "Foreign", + "56690": "On-Rails Shooter", + "61357": "Electronic Music", + "65443": "Adult Content", + "71389": "Spelling", + "87918": "Farming Sim", + "91114": "Shop Keeper", + "92092": "Jet", + "96359": "Skating", + "97376": "Cozy", + "102530": "Elf", + "117648": "8-bit Music", + "123332": "Bikes", + "129761": "ATV", + "143739": "Electronic", + "150626": "Gaming", + "158638": "Cricket", + "176981": "Battle Royale", + "180368": "Faith", + "189941": "Instrumental Music", + "198631": "Mystery Dungeon", + "198913": "Motorbike", + "220585": "Colony Sim", + "233824": "Feature Film", + "252854": "BMX", + "255534": "Automation", + "323922": "Musou", + "324176": "Hockey", + "337964": "Rock Music", + "348922": "Steam Machine", + "353880": "Looter Shooter", + "363767": "Snooker", + "379975": "Clicker", + "454187": "Traditional Roguelike", + "552282": "Wholesome", + "603297": "Hardware", + "615955": "Idler", + "620519": "Hero Shooter", + "745697": "Social Deduction", + "769306": "Escape Room", + "776177": "360 Video", + "791774": "Card Battler", + "847164": "Volleyball", + "856791": "Asymmetric VR", + "916648": "Creature Collector", + "922563": "Roguevania", + "1003823": "Profile Features Limited", + "1023537": "Boomer Shooter", + "1084988": "Auto Battler", + "1091588": "Roguelike Deckbuilder", + "1100686": "Outbreak Sim", + "1100687": "Automobile Sim", + "1100688": "Medical Sim", + "1100689": "Open World Survival Craft", + "1199779": "Extraction Shooter", + "1220528": "Hobby Sim", + "1254546": "Football (Soccer)", + "1254552": "Football (American)", + "1368160": "AI Content Disclosed", + } +} \ No newline at end of file diff --git a/packages/core/src/client/types.ts b/packages/core/src/client/types.ts index 16ec89bb..38903500 100644 --- a/packages/core/src/client/types.ts +++ b/packages/core/src/client/types.ts @@ -160,9 +160,9 @@ export interface AppConfig { export interface AppDepots { branches: AppDepotBranches; privatebranches: Record; - [depotId: string]: DepotEntry - | AppDepotBranches - | Record; + [depotId: string]: DepotEntry + | AppDepotBranches + | Record; } @@ -284,16 +284,27 @@ export type GenreType = { export interface AppInfo { name: string; slug: string; + images: { + logo: string; + backdrop: string; + poster: string; + banner: string; + screenshots: string[]; + icon: string; + } + links: string[] | null; score: number; - gameid: string; + id: string; releaseDate: Date; - description: string; + description: string | null; compatibility: "low" | "mid" | "high" | "unknown"; controllerSupport: "partial" | "full" | "unknown"; primaryGenre: string | null; size: { downloadSize: number; sizeOnDisk: number }; tags: Array<{ name: string; slug: string; type: "tag" }>; genres: Array<{ type: "genre"; name: string; slug: string }>; + categories: Array<{ name: string; slug: string; type: "categorie" }>; + franchises: Array<{ name: string; slug: string; type: "franchise" }>; developers: Array<{ name: string; slug: string; type: "developer" }>; publishers: Array<{ name: string; slug: string; type: "publisher" }>; } @@ -341,4 +352,249 @@ export interface Shot { export interface RankedShot { url: string; score: number; -} \ No newline at end of file +} + +export interface SteamPlayerSummaryResponse { + response: { + players: SteamPlayerSummary[]; + }; +} + +export interface SteamPlayerSummary { + steamid: string; + communityvisibilitystate: number; + profilestate?: number; + personaname: string; + profileurl: string; + avatar: string; + avatarmedium: string; + avatarfull: string; + avatarhash: string; + lastlogoff?: number; + personastate: number; + realname?: string; + primaryclanid?: string; + timecreated: number; + personastateflags?: number; + loccountrycode?: string; +} + +export interface SteamPlayerBansResponse { + players: SteamPlayerBan[]; +} + +export interface SteamPlayerBan { + SteamId: string; + CommunityBanned: boolean; + VACBanned: boolean; + NumberOfVACBans: number; + DaysSinceLastBan: number; + NumberOfGameBans: number; + EconomyBan: 'none' | 'probation' | 'banned'; // Enum based on known possible values +} + +export type SteamAccount = { + id: string; + name: string; + realName: string | null; + steamMemberSince: Date; + avatarHash: string; + limitations: { + isLimited: boolean; + tradeBanState: 'none' | 'probation' | 'banned'; + isVacBanned: boolean; + visibilityState: number; + privacyState: 'public' | 'private' | 'friendsonly'; + }; + profileUrl: string; + lastSyncedAt: Date; +}; + +export interface SteamFriendsListResponse { + friendslist: { + friends: SteamFriend[]; + }; +} + +export interface SteamFriend { + steamid: string; + relationship: 'friend'; // could expand this if Steam ever adds more types + friend_since: number; // Unix timestamp (seconds) +} + +export interface SteamOwnedGamesResponse { + response: { + game_count: number; + games: SteamOwnedGame[]; + }; +} + +export interface SteamOwnedGame { + appid: number; + name: string; + playtime_forever: number; + img_icon_url: string; + + playtime_windows_forever?: number; + playtime_mac_forever?: number; + playtime_linux_forever?: number; + playtime_deck_forever?: number; + + rtime_last_played?: number; // Unix timestamp + content_descriptorids?: number[]; + playtime_disconnected?: number; + has_community_visible_stats?: boolean; +} + +/** + * The shape of the parsed Steam profile information. + */ +export interface ProfileInfo { + steamID64: string; + isLimited: boolean; + privacyState: 'public' | 'private' | 'friendsonly' | string; + visibility: string; +} + +export interface SteamStoreResponse { + response: { + store_items: SteamStoreItem[]; + }; +} + +export interface SteamStoreItem { + item_type: number; + id: number; + success: number; + visible: boolean; + name: string; + store_url_path: string; + appid: number; + type: number; + tagids: number[]; + categories: { + supported_player_categoryids?: number[]; + feature_categoryids?: number[]; + controller_categoryids?: number[]; + }; + reviews: { + summary_filtered: { + review_count: number; + percent_positive: number; + review_score: number; + review_score_label: string; + }; + }; + basic_info: { + short_description?: string; + publishers: SteamCreator[]; + developers: SteamCreator[]; + franchises?: SteamCreator[]; + }; + tags: { + tagid: number; + weight: number; + }[]; + assets: SteamAssets; + assets_without_overrides: SteamAssets; + release: { + steam_release_date: number; + }; + platforms: { + windows: boolean; + mac: boolean; + steamos_linux: boolean; + vr_support: Record; + steam_deck_compat_category?: number; + steam_os_compat_category?: number; + }; + best_purchase_option: PurchaseOption; + purchase_options: PurchaseOption[]; + screenshots: { + all_ages_screenshots: { + filename: string; + ordinal: number; + }[]; + }; + trailers: { + highlights: Trailer[]; + }; + supported_languages: SupportedLanguage[]; + full_description: string; + links?: { + link_type: number; + url: string; + }[]; +} + +export interface SteamCreator { + name: string; + creator_clan_account_id: number; +} + +export interface SteamAssets { + asset_url_format: string; + main_capsule: string; + small_capsule: string; + header: string; + page_background: string; + hero_capsule: string; + hero_capsule_2x: string; + library_capsule: string; + library_capsule_2x: string; + library_hero: string; + library_hero_2x: string; + community_icon: string; + page_background_path: string; + raw_page_background: string; +} + +export interface PurchaseOption { + packageid?: number; + bundleid?: number; + purchase_option_name: string; + final_price_in_cents: string; + original_price_in_cents: string; + formatted_final_price: string; + formatted_original_price: string; + discount_pct: number; + active_discounts: ActiveDiscount[]; + user_can_purchase_as_gift: boolean; + hide_discount_pct_for_compliance: boolean; + included_game_count: number; + bundle_discount_pct?: number; + price_before_bundle_discount?: string; + formatted_price_before_bundle_discount?: string; +} + +export interface ActiveDiscount { + discount_amount: string; + discount_description: string; + discount_end_date: number; +} + +export interface Trailer { + trailer_name: string; + trailer_url_format: string; + trailer_category: number; + trailer_480p: TrailerFile[]; + trailer_max: TrailerFile[]; + microtrailer: TrailerFile[]; + screenshot_medium: string; + screenshot_full: string; + trailer_base_id: number; + all_ages: boolean; +} + +export interface TrailerFile { + filename: string; + type: string; +} + +export interface SupportedLanguage { + elanguage: number; + eadditionallanguage: number; + supported: boolean; + full_audio: boolean; + subtitles: boolean; +} diff --git a/packages/core/src/client/utils.ts b/packages/core/src/client/utils.ts index fb825707..0bbc1f67 100644 --- a/packages/core/src/client/utils.ts +++ b/packages/core/src/client/utils.ts @@ -9,6 +9,7 @@ import type { CompareResult, RankedShot, Shot, + ProfileInfo, } from "./types"; import crypto from 'crypto'; import pLimit from 'p-limit'; @@ -18,6 +19,7 @@ import { LRUCache } from 'lru-cache'; import sanitizeHtml from 'sanitize-html'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; +import { parseStringPromise } from "xml2js"; import sharp, { type Metadata } from 'sharp'; import AbortController from 'abort-controller'; import fetch, { RequestInit } from 'node-fetch'; @@ -90,29 +92,21 @@ export namespace Utils { // --- Optimized Box Art creation --- export async function createBoxArtBuffer( - assets: LibraryAssetsFull, - appid: number | string, + logoUrl: string, + backgroundUrl: string, logoPercent = 0.9 ): Promise { - const base = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${appid}`; - const pick = (key: string) => { - const set = assets[key]; - const path = set?.image2x?.english || set?.image?.english; - if (!path) throw new Error(`Missing asset for ${key}`); - return `${base}/${path}`; - }; - const [bgBuf, logoBuf] = await Promise.all([ downloadLimit(() => - fetchBuffer(pick('library_hero')) + fetchBuffer(backgroundUrl) .catch(error => { - console.error(`Failed to download hero image for ${appid}:`, error); + console.error(`Failed to download hero image from ${backgroundUrl}:`, error); throw new Error(`Failed to create box art: hero image unavailable`); }), ), - downloadLimit(() => fetchBuffer(pick('library_logo')) + downloadLimit(() => fetchBuffer(logoUrl) .catch(error => { - console.error(`Failed to download logo image for ${appid}:`, error); + console.error(`Failed to download logo image from ${logoUrl}:`, error); throw new Error(`Failed to create box art: logo image unavailable`); }), ), @@ -182,9 +176,11 @@ export namespace Utils { export function createSlug(name: string): string { return name .toLowerCase() - .replace(/[^\w\s -]/g, "") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") + .normalize("NFKD") // Normalize to decompose accented characters + .replace(/[^\p{L}\p{N}\s-]/gu, '') // Keep Unicode letters, numbers, spaces, and hyphens + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Collapse multiple hyphens + .replace(/^-+|-+$/g, '') // Trim leading/trailing hyphens .trim(); } @@ -328,16 +324,26 @@ export namespace Utils { export function compatibilityType(type?: string): "low" | "mid" | "high" | "unknown" { switch (type) { case "1": - return "low"; + return "high"; case "2": return "mid"; case "3": - return "high"; + return "low"; default: return "unknown"; } } + + export function estimateRatingFromSummary( + reviewCount: number, + percentPositive: number + ): number { + const positiveVotes = Math.round((percentPositive / 100) * reviewCount); + const negativeVotes = reviewCount - positiveVotes; + return getRating(positiveVotes, negativeVotes); + } + export function mapGameTags< T extends string = "tag" >( @@ -353,6 +359,20 @@ export namespace Utils { return result; } + export function createType< + T extends "developer" | "publisher" | "franchise" | "tag" | "categorie" | "genre" + >( + names: string[], + type: T + ) { + return names + .map(name => ({ + type, + name: name.trim(), + slug: createSlug(name.trim()) + })); + } + /** * Create a tag object with name, slug, and type * @typeparam T Literal type of the `type` field (defaults to 'tag') @@ -380,17 +400,39 @@ export namespace Utils { .toLowerCase(); } + function isDepotEntry(e: any): e is DepotEntry { + return ( + e != null && + typeof e === 'object' && + 'manifests' in e && + e.manifests != null && + typeof e.manifests.public?.download === 'string' + ); + } + export function getPublicDepotSizes(depots: AppDepots) { - const sum = { download: 0, size: 0 }; - for (const key in depots) { + let download = 0; + let size = 0; + + for (const key of Object.keys(depots)) { if (key === 'branches' || key === 'privatebranches') continue; const entry = depots[key] as DepotEntry; - if ('manifests' in entry && entry.manifests.public) { - sum.download += Number(entry.manifests.public.download); - sum.size += Number(entry.manifests.public.size); + if (!isDepotEntry(entry)) { + continue; } + + const dl = Number(entry.manifests.public.download); + const sz = Number(entry.manifests.public.size); + if (!Number.isFinite(dl) || !Number.isFinite(sz)) { + console.warn(`[getPublicDepotSizes] non-numeric size for depot ${key}`); + continue; + } + + download += dl; + size += sz; } - return { downloadSize: sum.download, sizeOnDisk: sum.size }; + + return { downloadSize: download, sizeOnDisk: size }; } export function parseGenres(str: string): GenreType[] { @@ -419,4 +461,64 @@ export namespace Utils { return cleaned.trim() } + + /** + * Fetches and parses a single Steam community profile XML. + * @param steamIdOrVanity - The 64-bit SteamID or vanity name. + * @returns Promise resolving to ProfileInfo. + */ + export async function fetchProfileInfo( + steamIdOrVanity: string + ): Promise { + const isNumericId = /^\d+$/.test(steamIdOrVanity); + const path = isNumericId ? `profiles/${steamIdOrVanity}` : `id/${steamIdOrVanity}`; + const url = `https://steamcommunity.com/${path}/?xml=1`; + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${steamIdOrVanity}: HTTP ${response.status}`); + } + + const xml = await response.text(); + const { profile } = await parseStringPromise(xml, { + explicitArray: false, + trim: true, + mergeAttrs: true + }) as { profile: any }; + + // Extract fields (fall back to limitedAccount tag if needed) + const limitedFlag = profile.isLimitedAccount ?? profile.limitedAccount; + const isLimited = limitedFlag === '1'; + + return { + isLimited, + steamID64: profile.steamID64, + privacyState: profile.privacyState, + visibility: profile.visibilityState + }; + } + + /** + * Batch-fetches multiple Steam profiles in parallel. + * @param idsOrVanities - Array of SteamID64 strings or vanity names. + * @returns Promise resolving to a record mapping each input to its ProfileInfo or an error. + */ + export async function fetchProfilesInfo( + idsOrVanities: string[] + ): Promise> { + const results = await Promise.all( + idsOrVanities.map(async (input) => { + try { + const info = await fetchProfileInfo(input); + return { input, result: info }; + } catch (err) { + return { input, result: { error: (err as Error).message } }; + } + }) + ); + + return new Map( + results.map(({ input, result }) => [input, result] as [string, ProfileInfo | { error: string }]) + ); + } } \ No newline at end of file diff --git a/packages/core/src/common.ts b/packages/core/src/common.ts index e827472b..90114314 100644 --- a/packages/core/src/common.ts +++ b/packages/core/src/common.ts @@ -1,5 +1,5 @@ -import { sql } from "drizzle-orm"; import "zod-openapi/extend"; +import { sql } from "drizzle-orm"; export namespace Common { export const IdDescription = `Unique object identifier. diff --git a/packages/core/src/credentials/credentials.sql.ts b/packages/core/src/credentials/credentials.sql.ts deleted file mode 100644 index 698931f4..00000000 --- a/packages/core/src/credentials/credentials.sql.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { steamTable } from "../steam/steam.sql"; -import { pgTable, primaryKey, varchar } from "drizzle-orm/pg-core"; -import { encryptedText, ulid, timestamps, utc } from "../drizzle/types"; - -export const steamCredentialsTable = pgTable( - "steam_account_credentials", - { - ...timestamps, - id: ulid("id").notNull(), - steamID: varchar("steam_id", { length: 255 }) - .notNull() - .references(() => steamTable.id, { - onDelete: "cascade" - }), - refreshToken: encryptedText("refresh_token") - .notNull(), - expiry: utc("expiry").notNull(), - username: varchar("username", { length: 255 }).notNull(), - }, - (table) => [ - primaryKey({ - columns: [table.steamID, table.id] - }) - ] -) \ No newline at end of file diff --git a/packages/core/src/credentials/index.ts b/packages/core/src/credentials/index.ts deleted file mode 100644 index 16742719..00000000 --- a/packages/core/src/credentials/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { z } from "zod"; -import { Resource } from "sst"; -import { bus } from "sst/aws/bus"; -import { createEvent } from "../event"; -import { createID, fn } from "../utils"; -import { eq, and, isNull, gt } from "drizzle-orm"; -import { createSelectSchema } from "drizzle-zod"; -import { steamCredentialsTable } from "./credentials.sql"; -import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; - -export namespace Credentials { - export const Info = createSelectSchema(steamCredentialsTable) - .omit({ timeCreated: true, timeDeleted: true, timeUpdated: true }) - .extend({ - accessToken: z.string(), - cookies: z.string().array() - }) - - export type Info = z.infer; - - export const Events = { - New: createEvent( - "new_credentials.added", - z.object({ - steamID: Info.shape.steamID, - }), - ), - }; - - export const create = fn( - Info - .omit({ accessToken: true, cookies: true, expiry: true }) - .partial({ id: true }), - (input) => { - const part = input.refreshToken.split('.')[1] as string - - const payload = JSON.parse(Buffer.from(part, 'base64').toString()); - - return createTransaction(async (tx) => { - const id = input.id ?? createID("credentials") - await tx - .insert(steamCredentialsTable) - .values({ - id, - steamID: input.steamID, - username: input.username, - refreshToken: input.refreshToken, - expiry: new Date(payload.exp * 1000), - }) - await afterTx(async () => - await bus.publish(Resource.Bus, Events.New, { steamID: input.steamID }) - ); - return id - }) - }); - - export const fromSteamID = fn( - Info.shape.steamID, - (steamID) => - useTransaction(async (tx) => { - const now = new Date() - - const credential = await tx - .select() - .from(steamCredentialsTable) - .where( - and( - eq(steamCredentialsTable.steamID, steamID), - isNull(steamCredentialsTable.timeDeleted), - gt(steamCredentialsTable.expiry, now) - ) - ) - .execute() - .then(rows => rows.at(0)); - - if (!credential) return null; - - return serialize(credential); - }) - ); - - export function serialize( - input: typeof steamCredentialsTable.$inferSelect, - ) { - return { - id: input.id, - expiry: input.expiry, - steamID: input.steamID, - username: input.username, - refreshToken: input.refreshToken, - }; - } -} \ No newline at end of file diff --git a/packages/core/src/drizzle/types.ts b/packages/core/src/drizzle/types.ts index 8cb568f7..00f551d6 100644 --- a/packages/core/src/drizzle/types.ts +++ b/packages/core/src/drizzle/types.ts @@ -1,5 +1,4 @@ -import { Token } from "../utils"; -import { char, customType, timestamp as rawTs } from "drizzle-orm/pg-core"; +import { char, timestamp as rawTs } from "drizzle-orm/pg-core"; export const ulid = (name: string) => char(name, { length: 26 + 4 }); @@ -33,19 +32,6 @@ export const utc = (name: string) => // mode: "date" }); -export const encryptedText = - customType<{ data: string; driverData: string; }>({ - dataType() { - return 'text'; - }, - fromDriver(val) { - return Token.decrypt(val); - }, - toDriver(val) { - return Token.encrypt(val); - }, - }); - export const timestamps = { timeCreated: utc("time_created").notNull().defaultNow(), timeUpdated: utc("time_updated").notNull().defaultNow(), diff --git a/packages/core/src/examples.ts b/packages/core/src/examples.ts index 942c4e7f..a4f121c3 100644 --- a/packages/core/src/examples.ts +++ b/packages/core/src/examples.ts @@ -34,7 +34,7 @@ export namespace Examples { export const SteamAccount = { status: "online" as const, //offline,dnd(do not disturb) or playing - id: "74839300282033",// Primary key + id: "74839300282033",// Steam ID userID: User.id,// | null FK to User (null if not linked) name: "JD The 65th", username: "jdoe", @@ -55,11 +55,10 @@ export namespace Examples { export const Team = { id: Id("team"),// Primary key - name: "John's Console", // Team name (not null, unique) - ownerID: User.id, // FK to User who owns/created the team - slug: SteamAccount.profileUrl.toLowerCase(), + name: "John", // Team name (not null, unique) maxMembers: 3, inviteCode: "xwydjf", + ownerSteamID: SteamAccount.id, // FK to User who owns/created the team members: [SteamAccount] }; @@ -152,6 +151,9 @@ export namespace Examples { id: "1809540", slug: "nine-sols", name: "Nine Sols", + links:[ + "https://example.com" + ], controllerSupport: "full" as const, releaseDate: new Date("2024-05-29T06:53:24.000Z"), compatibility: "high" as const, @@ -205,6 +207,13 @@ export namespace Examples { slug: "redcandlegames" } ], + franchises: [], + categories: [ + { + name: "Partial Controller", + slug: "partial-controller" + } + ] } export const CommonImg = [ diff --git a/packages/core/src/friend/index.ts b/packages/core/src/friend/index.ts index 881eeb33..bb8ff38b 100644 --- a/packages/core/src/friend/index.ts +++ b/packages/core/src/friend/index.ts @@ -8,9 +8,9 @@ import { friendTable } from "./friend.sql"; import { userTable } from "../user/user.sql"; import { steamTable } from "../steam/steam.sql"; import { createSelectSchema } from "drizzle-zod"; +import { and, eq, isNull, sql } from "drizzle-orm"; import { groupBy, map, pipe, values } from "remeda"; import { ErrorCodes, VisibleError } from "../error"; -import { and, eq, isNull, sql } from "drizzle-orm"; import { createTransaction, useTransaction } from "../drizzle/transaction"; export namespace Friend { diff --git a/packages/core/src/library/index.ts b/packages/core/src/library/index.ts index 5f459ad5..0801eb75 100644 --- a/packages/core/src/library/index.ts +++ b/packages/core/src/library/index.ts @@ -23,20 +23,17 @@ export namespace Library { "library.queue", z.object({ appID: z.number(), - lastPlayed: z.date(), - timeAcquired: z.date(), + lastPlayed: z.date().nullable(), totalPlaytime: z.number(), - isFamilyShared: z.boolean(), - isFamilyShareable: z.boolean(), - }).array(), + }), ), }; export const add = fn( - Info.partial({ ownerID: true }), + Info.partial({ ownerSteamID: true }), async (input) => createTransaction(async (tx) => { - const ownerSteamID = input.ownerID ?? Actor.steamID() + const ownerSteamID = input.ownerSteamID ?? Actor.steamID() const result = await tx .select() @@ -44,7 +41,7 @@ export namespace Library { .where( and( eq(steamLibraryTable.baseGameID, input.baseGameID), - eq(steamLibraryTable.ownerID, ownerSteamID), + eq(steamLibraryTable.ownerSteamID, ownerSteamID), isNull(steamLibraryTable.timeDeleted) ) ) @@ -57,21 +54,17 @@ export namespace Library { await tx .insert(steamLibraryTable) .values({ - ownerID: ownerSteamID, + ownerSteamID: ownerSteamID, baseGameID: input.baseGameID, lastPlayed: input.lastPlayed, totalPlaytime: input.totalPlaytime, - timeAcquired: input.timeAcquired, - isFamilyShared: input.isFamilyShared }) .onConflictDoUpdate({ - target: [steamLibraryTable.ownerID, steamLibraryTable.baseGameID], + target: [steamLibraryTable.ownerSteamID, steamLibraryTable.baseGameID], set: { timeDeleted: null, lastPlayed: input.lastPlayed, - timeAcquired: input.timeAcquired, totalPlaytime: input.totalPlaytime, - isFamilyShared: input.isFamilyShared } }) @@ -87,7 +80,7 @@ export namespace Library { .set({ timeDeleted: sql`now()` }) .where( and( - eq(steamLibraryTable.ownerID, input.ownerID), + eq(steamLibraryTable.ownerSteamID, input.ownerSteamID), eq(steamLibraryTable.baseGameID, input.baseGameID), ) ) @@ -105,7 +98,7 @@ export namespace Library { .from(steamLibraryTable) .where( and( - eq(steamLibraryTable.ownerID, Actor.steamID()), + eq(steamLibraryTable.ownerSteamID, Actor.steamID()), isNull(steamLibraryTable.timeDeleted) ) ) diff --git a/packages/core/src/library/library.sql.ts b/packages/core/src/library/library.sql.ts index 0e046866..9740590f 100644 --- a/packages/core/src/library/library.sql.ts +++ b/packages/core/src/library/library.sql.ts @@ -1,7 +1,7 @@ -import { timestamps, utc, } from "../drizzle/types"; import { steamTable } from "../steam/steam.sql"; +import { timestamps, utc, } from "../drizzle/types"; import { baseGamesTable } from "../base-game/base-game.sql"; -import { boolean, index, integer, pgTable, primaryKey, varchar, } from "drizzle-orm/pg-core"; +import { index, integer, pgTable, primaryKey, varchar, } from "drizzle-orm/pg-core"; export const steamLibraryTable = pgTable( "game_libraries", @@ -12,20 +12,18 @@ export const steamLibraryTable = pgTable( .references(() => baseGamesTable.id, { onDelete: "cascade" }), - ownerID: varchar("owner_id", { length: 255 }) + ownerSteamID: varchar("owner_steam_id", { length: 255 }) .notNull() .references(() => steamTable.id, { onDelete: "cascade" }), - timeAcquired: utc("time_acquired").notNull(), - lastPlayed: utc("last_played").notNull(), + lastPlayed: utc("last_played"), totalPlaytime: integer("total_playtime").notNull(), - isFamilyShared: boolean("is_family_shared").notNull() }, (table) => [ primaryKey({ - columns: [table.baseGameID, table.ownerID] + columns: [table.baseGameID, table.ownerSteamID] }), - index("idx_game_libraries_owner_id").on(table.ownerID), + index("idx_game_libraries_owner_id").on(table.ownerSteamID), ], ); \ No newline at end of file diff --git a/packages/core/src/member/index.ts b/packages/core/src/member/index.ts deleted file mode 100644 index 68af3950..00000000 --- a/packages/core/src/member/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { z } from "zod"; -import { Actor } from "../actor"; -import { Common } from "../common"; -import { Examples } from "../examples"; -import { createID, fn } from "../utils"; -import { and, eq, isNull } from "drizzle-orm" -import { memberTable, RoleEnum } from "./member.sql"; -import { createTransaction, useTransaction } from "../drizzle/transaction"; - -export namespace Member { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Member.id, - }), - teamID: z.string().openapi({ - description: "Associated team identifier for this membership", - example: Examples.Member.teamID - }), - role: z.enum(RoleEnum.enumValues).openapi({ - description: "Assigned permission role within the team", - example: Examples.Member.role - }), - steamID: z.string().openapi({ - description: "Steam platform identifier for Steam account integration", - example: Examples.Member.steamID - }), - userID: z.string().nullable().openapi({ - description: "Optional associated user account identifier", - example: Examples.Member.userID - }), - }) - .openapi({ - ref: "Member", - description: "Team membership entity defining user roles and platform connections", - example: Examples.Member, - }); - - export type Info = z.infer; - - export const create = fn( - Info - .partial({ - id: true, - userID: true, - teamID: true - }), - (input) => - createTransaction(async (tx) => { - const id = input.id ?? createID("member"); - await tx.insert(memberTable).values({ - id, - role: input.role, - userID: input.userID, - steamID: input.steamID, - teamID: input.teamID ?? Actor.teamID(), - }) - - return id; - }), - ); - - export const fromTeamID = fn( - Info.shape.teamID, - (teamID) => - useTransaction((tx) => - tx - .select() - .from(memberTable) - .where( - and( - eq(memberTable.userID, Actor.userID()), - eq(memberTable.teamID, teamID), - isNull(memberTable.timeDeleted) - ) - ) - .execute() - .then(rows => rows.map(serialize).at(0)) - ) - - ) - - export const fromUserID = fn( - z.string(), - (userID) => - useTransaction((tx) => - tx - .select() - .from(memberTable) - .where( - and( - eq(memberTable.userID, userID), - eq(memberTable.teamID, Actor.teamID()), - isNull(memberTable.timeDeleted) - ) - ) - .execute() - .then(rows => rows.map(serialize).at(0)) - ) - - ) - - /** - * Converts a raw member database row into a standardized {@link Member.Info} object. - * - * @param input - The database row representing a member. - * @returns The member information formatted as a {@link Member.Info} object. - */ - export function serialize( - input: typeof memberTable.$inferSelect, - ): z.infer { - return { - id: input.id, - role: input.role, - userID: input.userID, - teamID: input.teamID, - steamID: input.steamID - }; - } - -} \ No newline at end of file diff --git a/packages/core/src/member/member.sql.ts b/packages/core/src/member/member.sql.ts deleted file mode 100644 index 5728c2b9..00000000 --- a/packages/core/src/member/member.sql.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { isNotNull } from "drizzle-orm"; -import { userTable } from "../user/user.sql"; -import { steamTable } from "../steam/steam.sql"; -import { timestamps, teamID, ulid } from "../drizzle/types"; -import { bigint, pgEnum, pgTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/pg-core"; - -export const RoleEnum = pgEnum("member_role", ["child", "adult"]) - -export const memberTable = pgTable( - "members", - { - ...teamID, - ...timestamps, - userID: ulid("user_id") - .references(() => userTable.id, { - onDelete: "cascade" - }), - steamID: varchar("steam_id", { length: 255 }) - .notNull() - .references(() => steamTable.id, { - onDelete: "cascade", - onUpdate: "restrict" - }), - role: RoleEnum("role").notNull(), - }, - (table) => [ - primaryKey({ columns: [table.id, table.teamID] }), - uniqueIndex("idx_member_steam_id").on(table.teamID, table.steamID), - uniqueIndex("idx_member_user_id") - .on(table.teamID, table.userID) - .where(isNotNull(table.userID)) - ], -); \ No newline at end of file diff --git a/packages/core/src/steam/index.ts b/packages/core/src/steam/index.ts index 7605fa21..cb766e2d 100644 --- a/packages/core/src/steam/index.ts +++ b/packages/core/src/steam/index.ts @@ -1,15 +1,12 @@ import { z } from "zod"; import { fn } from "../utils"; -import { Resource } from "sst"; import { Actor } from "../actor"; -import { bus } from "sst/aws/bus"; import { Common } from "../common"; -import { createEvent } from "../event"; import { Examples } from "../examples"; +import { createEvent } from "../event"; import { eq, and, isNull, desc } from "drizzle-orm"; import { steamTable, StatusEnum, Limitations } from "./steam.sql"; -import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; -import { teamTable } from "../team/team.sql"; +import { createTransaction, useTransaction } from "../drizzle/transaction"; export namespace Steam { export const Info = z @@ -34,14 +31,6 @@ export namespace Steam { description: "The steam community url of this account", example: Examples.SteamAccount.profileUrl }), - username: z.string() - .regex(/^[a-z0-9]{1,32}$/, "The Steam username is not slug friendly") - .nullable() - .openapi({ - description: "The unique username of this account", - example: Examples.SteamAccount.username - }) - .default("unknown"), realName: z.string().nullable().openapi({ description: "The real name behind of this Steam account", example: Examples.SteamAccount.realName @@ -76,7 +65,7 @@ export namespace Steam { "steam_account.created", z.object({ steamID: Info.shape.id, - userID: Info.shape.userID + userID: Info.shape.userID, }), ), Updated: createEvent( @@ -94,9 +83,9 @@ export namespace Steam { useUser: z.boolean(), }) .partial({ - useUser: true, userID: true, status: true, + useUser: true, lastSyncedAt: true }), (input) => @@ -107,8 +96,8 @@ export namespace Steam { .from(steamTable) .where( and( - eq(steamTable.id, input.id), - isNull(steamTable.timeDeleted) + isNull(steamTable.timeDeleted), + eq(steamTable.id, input.id) ) ) .execute() @@ -129,7 +118,6 @@ export namespace Steam { avatarHash: input.avatarHash, limitations: input.limitations, status: input.status ?? "offline", - username: input.username ?? "unknown", steamMemberSince: input.steamMemberSince, lastSyncedAt: input.lastSyncedAt ?? Common.utc(), }) @@ -151,8 +139,8 @@ export namespace Steam { .partial({ userID: true }), - (input) => - useTransaction(async (tx) => { + async (input) => + createTransaction(async (tx) => { const userID = input.userID ?? Actor.userID() await tx .update(steamTable) @@ -160,6 +148,12 @@ export namespace Steam { userID }) .where(eq(steamTable.id, input.steamID)); + + // await afterTx(async () => + // bus.publish(Resource.Bus, Events.Updated, { userID, steamID: input.steamID }) + // ); + + return input.steamID }) ) @@ -177,6 +171,26 @@ export namespace Steam { ) ) + export const confirmOwnerShip = fn( + z.string().min(1), + (userID) => + useTransaction((tx) => + tx + .select() + .from(steamTable) + .where( + and( + eq(steamTable.userID, userID), + eq(steamTable.id, Actor.steamID()), + isNull(steamTable.timeDeleted) + ) + ) + .orderBy(desc(steamTable.timeCreated)) + .execute() + .then((rows) => rows.map(serialize).at(0)) + ) + ) + export const fromSteamID = fn( z.string(), (steamID) => @@ -208,15 +222,14 @@ export namespace Steam { return { id: input.id, name: input.name, - userID: input.userID, status: input.status, - username: input.username, + userID: input.userID, realName: input.realName, + profileUrl: input.profileUrl, avatarHash: input.avatarHash, limitations: input.limitations, lastSyncedAt: input.lastSyncedAt, steamMemberSince: input.steamMemberSince, - profileUrl: input.profileUrl ? `https://steamcommunity.com/id/${input.profileUrl}` : null, }; } diff --git a/packages/core/src/steam/steam.sql.ts b/packages/core/src/steam/steam.sql.ts index a924cff5..658797b3 100644 --- a/packages/core/src/steam/steam.sql.ts +++ b/packages/core/src/steam/steam.sql.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { userTable } from "../user/user.sql"; -import { timestamps, ulid, utc } from "../drizzle/types"; +import { id, timestamps, ulid, utc } from "../drizzle/types"; import { pgTable, varchar, pgEnum, json, unique } from "drizzle-orm/pg-core"; export const StatusEnum = pgEnum("steam_status", ["online", "offline", "dnd", "playing"]) @@ -32,11 +32,7 @@ export const steamTable = pgTable( steamMemberSince: utc("member_since").notNull(), name: varchar("name", { length: 255 }).notNull(), profileUrl: varchar("profile_url", { length: 255 }), - username: varchar("username", { length: 255 }).notNull(), avatarHash: varchar("avatar_hash", { length: 255 }).notNull(), limitations: json("limitations").$type().notNull(), - }, - (table) => [ - unique("idx_steam_username").on(table.username) - ] + } ); \ No newline at end of file diff --git a/packages/core/src/team/index.ts b/packages/core/src/team/index.ts deleted file mode 100644 index 2bfed912..00000000 --- a/packages/core/src/team/index.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { z } from "zod"; -import { Steam } from "../steam"; -import { Actor } from "../actor"; -import { Common } from "../common"; -import { teamTable } from "./team.sql"; -import { Examples } from "../examples"; -import { and, eq, isNull } from "drizzle-orm"; -import { steamTable } from "../steam/steam.sql"; -import { createID, fn, Invite } from "../utils"; -import { memberTable } from "../member/member.sql"; -import { groupBy, pipe, values, map } from "remeda"; -import { createTransaction, useTransaction, type Transaction } from "../drizzle/transaction"; - -export namespace Team { - export const Info = z - .object({ - id: z.string().openapi({ - description: Common.IdDescription, - example: Examples.Team.id, - }), - slug: z.string().regex(/^[a-z0-9-]{1,32}$/, "Use a URL friendly name.").openapi({ - description: "URL-friendly unique username (lowercase alphanumeric with hyphens)", - example: Examples.Team.slug - }), - name: z.string().openapi({ - description: "Display name of the team", - example: Examples.Team.name - }), - ownerID: z.string().openapi({ - description: "Unique identifier of the team owner", - example: Examples.Team.ownerID - }), - maxMembers: z.number().openapi({ - description: "Maximum allowed team members based on subscription tier", - example: Examples.Team.maxMembers - }), - inviteCode: z.string().openapi({ - description: "Unique invitation code used for adding new team members", - example: Examples.Team.inviteCode - }), - members: Steam.Info.array().openapi({ - description: "All the team members in this team", - example: Examples.Team.members - }) - }) - .openapi({ - ref: "Team", - description: "Team entity containing core team information and settings", - example: Examples.Team, - }); - - export type Info = z.infer; - - /** - * Generates a unique team invite code - * @param length The length of the invite code - * @param maxAttempts Maximum number of attempts to generate a unique code - * @returns A promise resolving to a unique invite code - */ - async function createUniqueTeamInviteCode( - tx: Transaction, - length: number = 8, - maxAttempts: number = 5 - ): Promise { - let attempts = 0; - - while (attempts < maxAttempts) { - const code = Invite.generateCode(length); - - const teams = - await tx - .select() - .from(teamTable) - .where(eq(teamTable.inviteCode, code)) - .execute() - - if (teams.length === 0) { - return code; - } - - attempts++; - } - - // If we've exceeded max attempts, add timestamp to ensure uniqueness - const timestampSuffix = Date.now().toString(36).slice(-4); - const baseCode = Invite.generateCode(length - 4); - return baseCode + timestampSuffix; - } - - export const create = fn( - Info - .omit({ members: true }) - .partial({ - id: true, - inviteCode: true, - maxMembers: true, - ownerID: true - }), - async (input) => - createTransaction(async (tx) => { - const inviteCode = await createUniqueTeamInviteCode(tx) - const id = input.id ?? createID("team"); - await tx - .insert(teamTable) - .values({ - id, - inviteCode, - slug: input.slug, - name: input.name, - ownerID: input.ownerID ?? Actor.userID(), - maxMembers: input.maxMembers ?? 1, - }) - .onConflictDoUpdate({ - target: [teamTable.slug], - set: { - timeDeleted: null - } - }) - - return id; - }) - ) - - export const list = () => - useTransaction(async (tx) => - tx - .select({ - steam_accounts: steamTable, - teams: teamTable - }) - .from(teamTable) - .innerJoin(memberTable, eq(memberTable.teamID, teamTable.id)) - .innerJoin(steamTable, eq(memberTable.steamID, steamTable.id)) - .where( - and( - eq(memberTable.userID, Actor.userID()), - isNull(memberTable.timeDeleted), - isNull(steamTable.timeDeleted), - isNull(teamTable.timeDeleted), - ), - ) - .execute() - .then((rows) => serialize(rows)) - ) - - export const fromSlug = fn( - Info.shape.slug, - (slug) => - useTransaction((tx) => - tx - .select() - .from(teamTable) - .innerJoin(memberTable, eq(memberTable.teamID, teamTable.id)) - .innerJoin(steamTable, eq(memberTable.steamID, steamTable.id)) - .where( - and( - eq(memberTable.userID, Actor.userID()), - isNull(memberTable.timeDeleted), - isNull(steamTable.timeDeleted), - isNull(teamTable.timeDeleted), - eq(teamTable.slug, slug), - ) - ) - .then((rows) => serialize(rows).at(0)) - ) - ) - - export function serialize( - input: { teams: typeof teamTable.$inferSelect; steam_accounts: typeof steamTable.$inferSelect | null }[] - ): z.infer[] { - return pipe( - input, - groupBy((row) => row.teams.id), - values(), - map((group) => ({ - id: group[0].teams.id, - slug: group[0].teams.slug, - name: group[0].teams.name, - ownerID: group[0].teams.ownerID, - maxMembers: group[0].teams.maxMembers, - inviteCode: group[0].teams.inviteCode, - members: group.map(i => i.steam_accounts) - .filter((c): c is typeof steamTable.$inferSelect => Boolean(c)) - .map((item) => Steam.serialize(item)) - })), - ) - } -} \ No newline at end of file diff --git a/packages/core/src/team/team.sql.ts b/packages/core/src/team/team.sql.ts deleted file mode 100644 index 37d55105..00000000 --- a/packages/core/src/team/team.sql.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { timestamps, id, ulid } from "../drizzle/types"; -import { - varchar, - pgTable, - bigint, - unique, - uniqueIndex, -} from "drizzle-orm/pg-core"; -import { userTable } from "../user/user.sql"; -import { steamTable } from "../steam/steam.sql"; - -export const teamTable = pgTable( - "teams", - { - ...id, - ...timestamps, - name: varchar("name", { length: 255 }).notNull(), - ownerID: ulid("owner_id") - .notNull() - .references(() => userTable.id, { - onDelete: "cascade" - }), - inviteCode: varchar("invite_code", { length: 10 }).notNull(), - slug: varchar("slug", { length: 255 }) - .notNull() - .references(() => steamTable.username, { - onDelete: "cascade" - }), - maxMembers: bigint("max_members", { mode: "number" }).notNull(), - }, - (team) => [ - uniqueIndex("idx_team_slug").on(team.slug), - unique("idx_team_invite_code").on(team.inviteCode) - ] -); \ No newline at end of file diff --git a/packages/core/src/user/index.ts b/packages/core/src/user/index.ts index 0d679018..4b86c3cf 100644 --- a/packages/core/src/user/index.ts +++ b/packages/core/src/user/index.ts @@ -1,15 +1,13 @@ import { z } from "zod"; -import { Resource } from "sst"; -import { bus } from "sst/aws/bus"; import { Common } from "../common"; import { createEvent } from "../event"; import { Polar } from "../polar/index"; import { createID, fn } from "../utils"; import { userTable } from "./user.sql"; import { Examples } from "../examples"; -import { and, eq, isNull, asc} from "drizzle-orm"; +import { and, eq, isNull, asc } from "drizzle-orm"; import { ErrorCodes, VisibleError } from "../error"; -import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; +import { createTransaction, useTransaction } from "../drizzle/transaction"; export namespace User { export const Info = z diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index bb2340f1..02816fb5 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,6 +1,5 @@ +export * from "./id" export * from "./fn" export * from "./log" -export * from "./id" export * from "./invite" -export * from "./token" export * from "./helper" \ No newline at end of file diff --git a/packages/core/src/utils/token.ts b/packages/core/src/utils/token.ts deleted file mode 100644 index b34971cb..00000000 --- a/packages/core/src/utils/token.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { z } from 'zod'; -import { fn } from './fn'; -import crypto from 'crypto'; -import { Resource } from 'sst'; - -// This is a 32-character random ASCII string -const rawKey = Resource.SteamEncryptionKey.value; - -// Turn it into exactly 32 bytes via UTF-8 -const key = Buffer.from(rawKey, 'utf8'); -if (key.length !== 32) { - throw new Error( - `SteamEncryptionKey must be exactly 32 bytes; got ${key.length}` - ); -} - -const ENCRYPTION_IV_LENGTH = 12; // 96 bits for GCM - -export namespace Token { - export const encrypt = fn( - z.string().min(4), - (token) => { - const iv = crypto.randomBytes(ENCRYPTION_IV_LENGTH); - const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); - - const ciphertext = Buffer.concat([ - cipher.update(token, 'utf8'), - cipher.final(), - ]); - const tag = cipher.getAuthTag(); - - return ['v1', iv.toString('hex'), tag.toString('hex'), ciphertext.toString('hex')].join(':'); - }); - - export const decrypt = fn( - z.string().min(4), - (data) => { - const [version, ivHex, tagHex, ciphertextHex] = data.split(':'); - if (version !== 'v1' || !ivHex || !tagHex || !ciphertextHex) { - throw new Error('Invalid token format'); - } - - const iv = Buffer.from(ivHex, 'hex'); - const tag = Buffer.from(tagHex, 'hex'); - const ciphertext = Buffer.from(ciphertextHex, 'hex'); - - const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); - decipher.setAuthTag(tag); - - const plaintext = Buffer.concat([ - decipher.update(ciphertext), - decipher.final(), - ]); - - return plaintext.toString('utf8'); - }); - -} \ No newline at end of file diff --git a/packages/functions/package.json b/packages/functions/package.json index 2d3a293d..fc3dc7e3 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -14,6 +14,9 @@ "peerDependencies": { "typescript": "^5" }, + "exports": { + "./*": "./src/*.ts" + }, "dependencies": { "@actor-core/bun": "^0.8.0", "@actor-core/file-system": "^0.8.0", @@ -27,4 +30,4 @@ "steamcommunity": "^3.48.6", "steamid": "^2.1.0" } -} +} \ No newline at end of file diff --git a/packages/functions/src/api/account.ts b/packages/functions/src/api/account.ts index e213b468..a2140644 100644 --- a/packages/functions/src/api/account.ts +++ b/packages/functions/src/api/account.ts @@ -20,7 +20,7 @@ export namespace AccountApi { schema: Result( Account.Info.openapi({ description: "User account information", - example: { ...Examples.User, teams: [Examples.Team] } + example: { ...Examples.User, profiles: [Examples.SteamAccount] } }) ), }, diff --git a/packages/functions/src/api/index.ts b/packages/functions/src/api/index.ts index 86984e57..bc86bf97 100644 --- a/packages/functions/src/api/index.ts +++ b/packages/functions/src/api/index.ts @@ -79,9 +79,9 @@ app.get( }, TeamID: { type: "apiKey", - description: "The team ID to use for this query", + description: "The steam ID to use for this query", in: "header", - name: "x-nestri-team" + name: "x-nestri-steam" }, }, }, diff --git a/packages/functions/src/api/steam.ts b/packages/functions/src/api/steam.ts index bb5389ab..35f7d561 100644 --- a/packages/functions/src/api/steam.ts +++ b/packages/functions/src/api/steam.ts @@ -1,22 +1,19 @@ import { z } from "zod"; import { Hono } from "hono"; -import crypto from 'crypto'; import { Resource } from "sst"; -import { streamSSE } from "hono/streaming"; import { Actor } from "@nestri/core/actor"; -import SteamCommunity from "steamcommunity"; import { describeRoute } from "hono-openapi"; -import { Team } from "@nestri/core/team/index"; +import { User } from "@nestri/core/user/index"; import { Examples } from "@nestri/core/examples"; import { Steam } from "@nestri/core/steam/index"; -import { Member } from "@nestri/core/member/index"; +import { getCookie, setCookie } from "hono/cookie"; import { Client } from "@nestri/core/client/index"; +import { Friend } from "@nestri/core/friend/index"; import { Library } from "@nestri/core/library/index"; import { chunkArray } from "@nestri/core/utils/helper"; -import { ErrorResponses, validator, Result } from "./utils"; -import { Credentials } from "@nestri/core/credentials/index"; +import { ErrorCodes, VisibleError } from "@nestri/core/error"; import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; -import { LoginSession, EAuthTokenPlatformType } from "steam-session"; +import { ErrorResponses, validator, Result, notPublic } from "./utils"; const sqs = new SQSClient({}); @@ -45,273 +42,217 @@ export namespace SteamApi { 429: ErrorResponses[429], } }), + notPublic, async (c) => c.json({ data: await Steam.list() }) ) - .get("/login", + .get("/callback/:id", + validator( + "param", + z.object({ + id: z.string().openapi({ + description: "ID of the user to login", + example: Examples.User.id, + }), + }), + ), + async (c) => { + const cookieID = getCookie(c, "user_id"); + + const userID = c.req.valid("param").id; + + if (!cookieID || cookieID !== userID) { + throw new VisibleError( + "authentication", + ErrorCodes.Authentication.UNAUTHORIZED, + "You should not be here" + ); + } + + const currentUser = await User.fromID(userID); + if (!currentUser) { + throw new VisibleError( + "not_found", + ErrorCodes.NotFound.RESOURCE_NOT_FOUND, + `User ${userID} not found` + ) + } + + const params = new URL(c.req.url).searchParams; + + // Verify OpenID response and get steamID + const steamID = await Client.verifyOpenIDResponse(params); + + // If verification failed, return error + if (!steamID) { + throw new VisibleError( + "authentication", + ErrorCodes.Authentication.UNAUTHORIZED, + "Invalid OpenID authentication response" + ); + } + + const user = (await Client.getUserInfo([steamID]))[0]; + + if (!user) { + throw new VisibleError( + "internal", + ErrorCodes.NotFound.RESOURCE_NOT_FOUND, + "Steam user data is missing" + ); + } + + const wasAdded = await Steam.create({ ...user, userID }); + + if (!wasAdded) { + // Update the owner of the Steam account + await Steam.updateOwner({ userID, steamID }) + } + + c.executionCtx.waitUntil((async () => { + try { + // Get friends info + const friends = await Client.getFriendsList(steamID); + + const friendSteamIDs = friends.friendslist.friends.map(f => f.steamid); + + // Steam API has a limit of requesting 100 friends at a go + const friendChunks = chunkArray(friendSteamIDs, 100); + + const settled = await Promise.allSettled( + friendChunks.map(async (friendIDs) => { + const friendsInfo = await Client.getUserInfo(friendIDs) + + return await Promise.all( + friendsInfo.map(async (friend) => { + const wasAdded = await Steam.create(friend); + + if (!wasAdded) { + console.log(`Friend ${friend.id} already exists`) + } + + await Friend.add({ friendSteamID: friend.id, steamID }) + + return friend.id + }) + ) + }) + ) + + settled + .filter(result => result.status === 'rejected') + .forEach(result => console.warn('[putFriends] failed:', (result as PromiseRejectedResult).reason)) + + const prod = (Resource.App.stage === "production" || Resource.App.stage === "dev") + + const friendIDs = [ + steamID, + ...(prod ? settled + .filter(result => result.status === "fulfilled") + .map(f => f.value) + .flat() : []) + ] + + await Promise.all( + friendIDs.map(async (currentSteamID) => { + // Get user library + const gameLibrary = await Client.getUserLibrary(currentSteamID); + + const queryLib = await Promise.allSettled( + gameLibrary.response.games.map(async (game) => { + await Actor.provide( + "steam", + { + steamID: currentSteamID, + }, + async () => { + const payload = await Library.Events.Queue.create({ + appID: game.appid, + lastPlayed: game.rtime_last_played ? new Date(game.rtime_last_played * 1000) : null, + totalPlaytime: game.playtime_forever + }); + + await sqs.send( + new SendMessageCommand({ + QueueUrl: Resource.LibraryQueue.url, + // Prevent bombarding Steam with requests at the same time + DelaySeconds: 10, + MessageBody: JSON.stringify(payload), + }) + ) + } + ) + }) + ) + + queryLib + .filter(i => i.status === "rejected") + .forEach(e => console.warn(`[pushUserLib]: Failed to push user library to queue: ${e.reason}`)) + }) + ) + } catch (error: any) { + console.error(`Failed to process Steam data for user ${userID}:`, error); + } + })()) + + return c.html( + ` + + ` + ) + } + ) + .get("/popup/:id", describeRoute({ tags: ["Steam"], - summary: "Login to Steam using QR code", - description: "Login to Steam using a QR code sent using Server Sent Events", + summary: "Login to Steam", + description: "Login to Steam in a popup", responses: { 400: ErrorResponses[400], 429: ErrorResponses[429], } }), validator( - "header", + "param", z.object({ - "accept": z.string() - .refine((v) => - v.toLowerCase() - .includes("text/event-stream") - ) - .openapi({ - description: "Client must accept Server Sent Events", - example: "text/event-stream" - }) - }) + id: z.string().openapi({ + description: "ID of the user to login", + example: Examples.User.id, + }), + }), ), - (c) => { - const currentUser = Actor.user() + async (c) => { + const userID = c.req.valid("param").id; - return streamSSE(c, async (stream) => { + const user = await User.fromID(userID); + if (!user) { + throw new VisibleError( + "not_found", + ErrorCodes.NotFound.RESOURCE_NOT_FOUND, + `User ${userID} not found` + ) + } - const session = new LoginSession(EAuthTokenPlatformType.MobileApp); + setCookie(c, "user_id", user.id); - session.loginTimeout = 30000; //30 seconds is typically when the url expires + const returnUrl = `${new URL(c.req.url).origin}/steam/callback/${userID}` - await stream.writeSSE({ - event: 'status', - data: JSON.stringify({ message: "connected to steam" }) - }) + const params = new URLSearchParams({ + 'openid.ns': 'http://specs.openid.net/auth/2.0', + 'openid.mode': 'checkid_setup', + 'openid.return_to': returnUrl, + 'openid.realm': new URL(returnUrl).origin, + 'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select', + 'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select', + 'user_id': user.id + }); - const challenge = await session.startWithQR(); - - await stream.writeSSE({ - event: 'challenge_url', - data: JSON.stringify({ url: challenge.qrChallengeUrl }) - }) - - return new Promise((resolve, reject) => { - session.on('remoteInteraction', async () => { - await stream.writeSSE({ - event: 'remote_interaction', - data: JSON.stringify({ message: "Looks like you've scanned the code! Now just approve the login." }), - }) - - await stream.writeSSE({ - event: 'status', - data: JSON.stringify({ message: "Looks like you've scanned the code! Now just approve the login." }), - }) - }); - - session.on('timeout', async () => { - console.log('This login attempt has timed out.'); - - await stream.writeSSE({ - event: 'status', - data: JSON.stringify({ message: "Your session timed out" }), - }) - - await stream.writeSSE({ - event: 'timed_out', - data: JSON.stringify({ success: false }), - }) - - await stream.close() - reject("Authentication timed out") - }); - - session.on('error', async (err) => { - // This should ordinarily not happen. This only happens in case there's some kind of unexpected error while - // polling, e.g. the network connection goes down or Steam chokes on something. - await stream.writeSSE({ - event: 'status', - data: JSON.stringify({ message: "Recieved an error while authenticating" }), - }) - - await stream.writeSSE({ - event: 'error', - data: JSON.stringify({ message: err.message }), - }) - - await stream.close() - reject(err.message) - }); - - - session.on('authenticated', async () => { - await stream.writeSSE({ - event: 'status', - data: JSON.stringify({ message: "Login successful" }) - }) - - await stream.writeSSE({ - event: 'login_success', - data: JSON.stringify({ success: true, }) - }) - - const username = session.accountName; - const accessToken = session.accessToken; - const refreshToken = session.refreshToken; - const steamID = session.steamID.toString(); - const cookies = await session.getWebCookies(); - - // Get user information - const community = new SteamCommunity(); - community.setCookies(cookies); - - const user = await Client.getUserInfo({ steamID, cookies }) - - const wasAdded = - await Steam.create({ - username, - id: steamID, - name: user.name, - realName: user.realName, - userID: currentUser.userID, - avatarHash: user.avatarHash, - steamMemberSince: user.memberSince, - profileUrl: user.customURL?.trim() || null, - limitations: { - isLimited: user.isLimitedAccount, - isVacBanned: user.vacBanned, - privacyState: user.privacyState as any, - visibilityState: Number(user.visibilityState), - tradeBanState: user.tradeBanState.toLowerCase() as any, - } - }) - - // Does not matter if the user is already there or has just been created, just store the credentials - await Credentials.create({ refreshToken, steamID, username }) - - let teamID: string | undefined - - if (wasAdded) { - const rawFirst = (user.name ?? username).trim().split(/\s+/)[0] ?? username; - - const firstName = rawFirst - .charAt(0) // first character - .toUpperCase() // make it uppercase - + rawFirst - .slice(1) // rest of the string - .toLowerCase(); - - // create a team - teamID = await Team.create({ - slug: username, - name: firstName, - ownerID: currentUser.userID, - }) - - // Add us as a member - await Actor.provide( - "system", - { teamID }, - async () => - await Member.create({ - role: "adult", - userID: currentUser.userID, - steamID - }) - ) - - } else { - // Update the owner of the Steam account - await Steam.updateOwner({ userID: currentUser.userID, steamID }) - const t = await Actor.provide( - "user", - currentUser, - async () => { - // Get the team associated with this username - const team = await Team.fromSlug(username); - // This should never happen - if (!team) throw Error(`Is Nestri okay???, we could not find the team with this slug ${username}`) - - teamID = team.id - - return team.id - } - ) - console.log("t",t) - console.log("teamID",teamID) - } - - await stream.writeSSE({ - event: 'team_slug', - data: JSON.stringify({ username }) - }) - - // Get game library in the background - c.executionCtx.waitUntil((async () => { - const games = await Client.getUserLibrary(accessToken); - - // Get a batch of 5 games each - const apps = games?.response?.apps || []; - if (apps.length === 0) { - console.info("[SteamApi] Is Steam okay? No games returned for user:", { steamID }); - return - } - - const chunkedGames = chunkArray(apps, 5); - // Get the batches to the queue - const processQueue = chunkedGames.map(async (chunk) => { - const myGames = chunk.map(i => { - return { - appID: i.appid, - totalPlaytime: i.rt_playtime, - isFamilyShareable: i.exclude_reason === 0, - lastPlayed: new Date(i.rt_last_played * 1000), - timeAcquired: new Date(i.rt_time_acquired * 1000), - isFamilyShared: !i.owner_steamids.includes(steamID) && i.exclude_reason === 0, - } - }) - - if (teamID) { - const deduplicationId = crypto - .createHash('md5') - .update(`${teamID}_${chunk.map(g => g.appid).join(',')}`) - .digest('hex'); - - await Actor.provide( - "member", - { - teamID, - steamID, - userID: currentUser.userID - }, - async () => { - const payload = await Library.Events.Queue.create(myGames); - - await sqs.send( - new SendMessageCommand({ - MessageGroupId: teamID, - QueueUrl: Resource.LibraryQueue.url, - MessageBody: JSON.stringify(payload), - MessageDeduplicationId: deduplicationId, - }) - ) - } - ) - } - }) - - const settled = await Promise.allSettled(processQueue) - - settled - .filter(r => r.status === "rejected") - .forEach(r => console.error("[LibraryQueue] enqueue failed:", (r as PromiseRejectedResult).reason)); - })()) - - await stream.close(); - - resolve(); - }) - }) - }) + return c.redirect(`https://steamcommunity.com/openid/login?${params.toString()}`, 302) } ) } \ No newline at end of file diff --git a/packages/functions/src/api/team.ts b/packages/functions/src/api/team.ts deleted file mode 100644 index 59782ed5..00000000 --- a/packages/functions/src/api/team.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { z } from "zod" -import { Hono } from "hono"; -import { describeRoute } from "hono-openapi"; -import { Team } from "@nestri/core/team/index"; -import { Examples } from "@nestri/core/examples"; -import { ErrorResponses, Result, validator } from "./utils"; -import { ErrorCodes, VisibleError } from "@nestri/core/error"; - -export namespace TeamApi { - export const route = new Hono() - .get("/", - describeRoute({ - tags: ["Team"], - summary: "List user teams", - description: "List the current user's team details", - responses: { - 200: { - content: { - "application/json": { - schema: Result( - Team.Info.array().openapi({ - description: "All team information", - example: [Examples.Team] - }) - ), - }, - }, - description: "All team details" - }, - 400: ErrorResponses[400], - 404: ErrorResponses[404], - 429: ErrorResponses[429], - } - }), - async (c) => - c.json({ - data: await Team.list() - }) - ) - .get("/:slug", - describeRoute({ - tags: ["Team"], - summary: "Get team by slug", - description: "Get the current user's team details, by its slug", - responses: { - 200: { - content: { - "application/json": { - schema: Result( - Team.Info.openapi({ - description: "Team details", - example: Examples.Team - }) - ), - }, - }, - description: "Team details" - }, - 400: ErrorResponses[400], - 404: ErrorResponses[404], - 429: ErrorResponses[429], - } - }), - validator( - "param", - z.object({ - slug: z.string().openapi({ - description: "SLug of the team to get", - example: Examples.Team.slug, - }), - }), - ), - async (c) => { - const teamSlug = c.req.valid("param").slug - - const team = await Team.fromSlug(teamSlug) - - if (!team) { - throw new VisibleError( - "not_found", - ErrorCodes.NotFound.RESOURCE_NOT_FOUND, - `Team ${teamSlug} not found` - ) - } - - return c.json({ - data: team - }) - } - ) -} \ No newline at end of file diff --git a/packages/functions/src/api/utils/auth.ts b/packages/functions/src/api/utils/auth.ts index 2fa6c1bb..ff388817 100644 --- a/packages/functions/src/api/utils/auth.ts +++ b/packages/functions/src/api/utils/auth.ts @@ -2,9 +2,9 @@ import { Resource } from "sst"; import { subjects } from "../../subjects"; import { Actor } from "@nestri/core/actor"; import { type MiddlewareHandler } from "hono"; +import { Steam } from "@nestri/core/steam/index"; import { createClient } from "@openauthjs/openauth/client"; import { ErrorCodes, VisibleError } from "@nestri/core/error"; -import { Member } from "@nestri/core/member/index"; const client = createClient({ clientID: "api", @@ -44,19 +44,19 @@ export const auth: MiddlewareHandler = async (c, next) => { } if (result.subject.type === "user") { - const teamID = c.req.header("x-nestri-team"); - if (!teamID) { + const steamID = c.req.header("x-nestri-steam"); + if (!steamID) { return Actor.provide(result.subject.type, result.subject.properties, next); } const userID = result.subject.properties.userID return Actor.provide( - "system", + "steam", { - teamID + steamID }, async () => { - const member = await Member.fromUserID(userID) - if (!member || !member.userID) { + const steamAcc = await Steam.confirmOwnerShip(userID) + if (!steamAcc) { throw new VisibleError( "authentication", ErrorCodes.Authentication.UNAUTHORIZED, @@ -66,9 +66,8 @@ export const auth: MiddlewareHandler = async (c, next) => { return Actor.provide( "member", { - steamID: member.steamID, - userID: member.userID, - teamID: member.teamID + steamID, + userID, }, next) }); diff --git a/packages/functions/src/events/index.ts b/packages/functions/src/events/index.ts index 215d78e1..501822da 100644 --- a/packages/functions/src/events/index.ts +++ b/packages/functions/src/events/index.ts @@ -1,117 +1,238 @@ +import "zod-openapi/extend"; import { Resource } from "sst"; import { bus } from "sst/aws/bus"; +import { Actor } from "@nestri/core/actor"; +import { Game } from "@nestri/core/game/index"; import { Steam } from "@nestri/core/steam/index"; import { Client } from "@nestri/core/client/index"; import { Images } from "@nestri/core/images/index"; -import { Friend } from "@nestri/core/friend/index"; +import { Library } from "@nestri/core/library/index"; import { BaseGame } from "@nestri/core/base-game/index"; -import { Credentials } from "@nestri/core/credentials/index"; -import { EAuthTokenPlatformType, LoginSession } from "steam-session"; +import { Categories } from "@nestri/core/categories/index"; import { PutObjectCommand, S3Client, HeadObjectCommand } from "@aws-sdk/client-s3"; const s3 = new S3Client({}); export const handler = bus.subscriber( - [Credentials.Events.New, BaseGame.Events.New], + [ + BaseGame.Events.New, + Steam.Events.Updated, + Steam.Events.Created, + BaseGame.Events.NewBoxArt, + BaseGame.Events.NewHeroArt, + ], async (event) => { console.log(event.type, event.properties, event.metadata); switch (event.type) { - case "new_credentials.added": { - const input = event.properties - const credentials = await Credentials.fromSteamID(input.steamID) - if (credentials) { - const session = new LoginSession(EAuthTokenPlatformType.MobileApp); + case "new_image.save": { + const input = event.properties; + const image = await Client.getImageInfo({ url: input.url, type: input.type }); - session.refreshToken = credentials.refreshToken; + await Images.create({ + type: image.type, + imageHash: image.hash, + baseGameID: input.appID, + position: image.position, + fileSize: image.fileSize, + sourceUrl: image.sourceUrl, + dimensions: image.dimensions, + extractedColor: image.averageColor, + }); - const cookies = await session.getWebCookies(); + try { + //Check whether the image already exists + await s3.send( + new HeadObjectCommand({ + Bucket: Resource.Storage.name, + Key: `images/${image.hash}`, + }) + ); - const friends = await Client.getFriendsList(cookies); - - const putFriends = friends.map(async (user) => { - const wasAdded = - await Steam.create({ - id: user.steamID.toString(), - name: user.name, - realName: user.realName, - avatarHash: user.avatarHash, - steamMemberSince: user.memberSince, - profileUrl: user.customURL?.trim() || null, - limitations: { - isLimited: user.isLimitedAccount, - isVacBanned: user.vacBanned, - tradeBanState: user.tradeBanState.toLowerCase() as any, - privacyState: user.privacyState as any, - visibilityState: Number(user.visibilityState) - } - }) - - if (!wasAdded) { - console.log(`Steam user ${user.steamID.toString()} already exists`) - } - - await Friend.add({ friendSteamID: user.steamID.toString(), steamID: input.steamID }) - }) - - const settled = await Promise.allSettled(putFriends); - - settled - .filter(result => result.status === 'rejected') - .forEach(result => console.warn('[putFriends] failed:', (result as PromiseRejectedResult).reason)) + } catch (e) { + // Save to s3 because it doesn't already exist + await s3.send( + new PutObjectCommand({ + Bucket: Resource.Storage.name, + Key: `images/${image.hash}`, + Body: image.buffer, + ...(image.format && { ContentType: `image/${image.format}` }), + Metadata: { + type: image.type, + appID: input.appID, + } + }) + ) } - break; - } - case "new_game.added": { - const input = event.properties - // Get images and save to s3 - const images = await Client.getImages(input.appID); - - (await Promise.allSettled( - images.map(async (image) => { - // Put the images into the db - await Images.create({ - type: image.type, - imageHash: image.hash, - baseGameID: input.appID, - position: image.position, - fileSize: image.fileSize, - sourceUrl: image.sourceUrl, - dimensions: image.dimensions, - extractedColor: image.averageColor, - }); - - try { - //Check whether the image already exists - await s3.send( - new HeadObjectCommand({ - Bucket: Resource.Storage.name, - Key: `images/${image.hash}`, - }) - ); - - } catch (e) { - // Save to s3 because it doesn't already exist - await s3.send( - new PutObjectCommand({ - Bucket: Resource.Storage.name, - Key: `images/${image.hash}`, - Body: image.buffer, - ...(image.format && { ContentType: `image/${image.format}` }), - Metadata: { - type: image.type, - appID: input.appID, - } - }) - ) - } - - }) - )) - .filter(i => i.status === "rejected") - .forEach(r => console.warn("[createImages] failed:", (r as PromiseRejectedResult).reason)); break; } + case "new_box_art_image.save": { + const input = event.properties; + + const image = await Client.createBoxArt(input); + + await Images.create({ + type: image.type, + imageHash: image.hash, + baseGameID: input.appID, + position: image.position, + fileSize: image.fileSize, + sourceUrl: image.sourceUrl, + dimensions: image.dimensions, + extractedColor: image.averageColor, + }); + + try { + //Check whether the image already exists + await s3.send( + new HeadObjectCommand({ + Bucket: Resource.Storage.name, + Key: `images/${image.hash}`, + }) + ); + + } catch (e) { + // Save to s3 because it doesn't already exist + await s3.send( + new PutObjectCommand({ + Bucket: Resource.Storage.name, + Key: `images/${image.hash}`, + Body: image.buffer, + ...(image.format && { ContentType: `image/${image.format}` }), + Metadata: { + type: image.type, + appID: input.appID, + } + }) + ) + } + + break; + } + case "new_hero_art_image.save": { + const input = event.properties; + + const images = await Client.createHeroArt(input); + + const settled = + await Promise.allSettled( + images.map(async (image) => { + await Images.create({ + type: image.type, + imageHash: image.hash, + baseGameID: input.appID, + position: image.position, + fileSize: image.fileSize, + sourceUrl: image.sourceUrl, + dimensions: image.dimensions, + extractedColor: image.averageColor, + }); + + try { + //Check whether the image already exists + await s3.send( + new HeadObjectCommand({ + Bucket: Resource.Storage.name, + Key: `images/${image.hash}`, + }) + ); + + } catch (e) { + // Save to s3 because it doesn't already exist + await s3.send( + new PutObjectCommand({ + Bucket: Resource.Storage.name, + Key: `images/${image.hash}`, + Body: image.buffer, + ...(image.format && { ContentType: `image/${image.format}` }), + Metadata: { + type: image.type, + appID: input.appID, + } + }) + ) + } + }) + ) + + settled + .filter(r => r.status === "rejected") + .forEach(r => console.warn("[processHeroArt] failed:", (r as PromiseRejectedResult).reason)); + + break; + } + // case "steam_account.updated": + // case "steam_account.created": { + // //Get user library and commit it to the db + // const steamID = event.properties.steamID; + + // await Actor.provide( + // event.metadata.actor.type, + // event.metadata.actor.properties, + // async () => { + // //Get user library + // const gameLibrary = await Client.getUserLibrary(steamID); + + // const myLibrary = new Map(gameLibrary.response.games.map(g => [g.appid, g])) + + // const queryLib = await Promise.allSettled( + // gameLibrary.response.games.map(async (game) => { + // return await Client.getAppInfo(game.appid.toString()) + // }) + // ) + + // queryLib + // .filter(i => i.status === "rejected") + // .forEach(e => console.warn(`[getAppInfo]: Failed to get game metadata: ${e.reason}`)) + + // const gameInfo = queryLib.filter(i => i.status === "fulfilled").map(f => f.value) + + // const queryGames = gameInfo.map(async (game) => { + // await BaseGame.create(game); + + // const allCategories = [...game.tags, ...game.genres, ...game.publishers, ...game.developers]; + + // const uniqueCategories = Array.from( + // new Map(allCategories.map(c => [`${c.type}:${c.slug}`, c])).values() + // ); + + // const gameSettled = await Promise.allSettled( + // uniqueCategories.map(async (cat) => { + // //Use a single db transaction to get or set the category + // await Categories.create({ + // type: cat.type, slug: cat.slug, name: cat.name + // }) + + // // Use a single db transaction to get or create the game + // await Game.create({ baseGameID: game.id, categorySlug: cat.slug, categoryType: cat.type }) + // }) + // ) + + // gameSettled + // .filter(r => r.status === "rejected") + // .forEach(r => console.warn("[uniqueCategories] failed:", (r as PromiseRejectedResult).reason)); + + // const currentGameInLibrary = myLibrary.get(parseInt(game.id)) + // if (currentGameInLibrary) { + // await Library.add({ + // baseGameID: game.id, + // lastPlayed: currentGameInLibrary.rtime_last_played ? new Date(currentGameInLibrary.rtime_last_played * 1000) : null, + // totalPlaytime: currentGameInLibrary.playtime_forever, + // }) + // } else { + // throw new Error(`Game is not in library, but was found in app info:${game.id}`) + // } + // }) + + // const settled = await Promise.allSettled(queryGames); + + // settled + // .filter(i => i.status === "rejected") + // .forEach(e => console.warn(`[gameCreate]: Failed to create game: ${e.reason}`)) + // }) + + // break; + // } } }, ); \ No newline at end of file diff --git a/packages/functions/src/queues/library.ts b/packages/functions/src/queues/library.ts index 131db642..af95060d 100644 --- a/packages/functions/src/queues/library.ts +++ b/packages/functions/src/queues/library.ts @@ -1,11 +1,14 @@ +import "zod-openapi/extend"; +import { Resource } from "sst"; +import { bus } from "sst/aws/bus"; import { SQSHandler } from "aws-lambda"; import { Actor } from "@nestri/core/actor"; import { Game } from "@nestri/core/game/index"; -import { Utils } from "@nestri/core/client/utils"; import { Client } from "@nestri/core/client/index"; import { Library } from "@nestri/core/library/index"; import { BaseGame } from "@nestri/core/base-game/index"; import { Categories } from "@nestri/core/categories/index"; +import { ImageTypeEnum } from "@nestri/core/images/images.sql"; export const handler: SQSHandler = async (event) => { for (const record of event.Records) { @@ -17,71 +20,103 @@ export const handler: SQSHandler = async (event) => { parsed.metadata.actor.type, parsed.metadata.actor.properties, async () => { - const processGames = parsed.properties.map(async (game) => { - // First check whether the base_game exists, if not get it - const appID = game.appID.toString() - const exists = await BaseGame.fromID(appID) + const game = parsed.properties + // First check whether the base_game exists, if not get it + const appID = game.appID.toString(); + const exists = await BaseGame.fromID(appID); - if (!exists) { - const appInfo = await Client.getAppInfo(appID); - const tags = appInfo.tags; + if (!exists) { + const appInfo = await Client.getAppInfo(appID); - await BaseGame.create({ - id: appID, - name: appInfo.name, - size: appInfo.size, - score: appInfo.score, - slug: appInfo.slug, - description: appInfo.description, - releaseDate: appInfo.releaseDate, - primaryGenre: appInfo.primaryGenre, - compatibility: appInfo.compatibility, - controllerSupport: appInfo.controllerSupport, - }) - - if (game.isFamilyShareable) { - tags.push(Utils.createTag("Family Share")) - } - - const allCategories = [...tags, ...appInfo.genres, ...appInfo.publishers, ...appInfo.developers] - - const uniqueCategories = Array.from( - new Map(allCategories.map(c => [`${c.type}:${c.slug}`, c])).values() - ); - - const settled = await Promise.allSettled( - uniqueCategories.map(async (cat) => { - //Use a single db transaction to get or set the category - await Categories.create({ - type: cat.type, slug: cat.slug, name: cat.name - }) - - // Use a single db transaction to get or create the game - await Game.create({ baseGameID: appID, categorySlug: cat.slug, categoryType: cat.type }) - }) - ) - - settled - .filter(r => r.status === "rejected") - .forEach(r => console.warn("[uniqueCategories] failed:", (r as PromiseRejectedResult).reason)); - } - - // Add to user's library - await Library.add({ - baseGameID: appID, - lastPlayed: game.lastPlayed, - timeAcquired: game.timeAcquired, - totalPlaytime: game.totalPlaytime, - isFamilyShared: game.isFamilyShared, + await BaseGame.create({ + id: appID, + name: appInfo.name, + size: appInfo.size, + slug: appInfo.slug, + links: appInfo.links, + score: appInfo.score, + description: appInfo.description, + releaseDate: appInfo.releaseDate, + primaryGenre: appInfo.primaryGenre, + compatibility: appInfo.compatibility, + controllerSupport: appInfo.controllerSupport, }) + + const allCategories = [...appInfo.tags, ...appInfo.genres, ...appInfo.publishers, ...appInfo.developers, ...appInfo.categories, ...appInfo.franchises] + + const uniqueCategories = Array.from( + new Map(allCategories.map(c => [`${c.type}:${c.slug}`, c])).values() + ); + + await Promise.all( + uniqueCategories.map(async (cat) => { + //Create category if it doesn't exist + await Categories.create({ + type: cat.type, slug: cat.slug, name: cat.name + }) + + //Create game if it doesn't exist + await Game.create({ baseGameID: appID, categorySlug: cat.slug, categoryType: cat.type }) + }) + ) + + const imageUrls = appInfo.images + + await Promise.all( + ImageTypeEnum.enumValues.map(async (type) => { + switch (type) { + case "backdrop": { + await bus.publish(Resource.Bus, BaseGame.Events.New, { appID, type: "backdrop", url: imageUrls.backdrop }) + break; + } + case "banner": { + await bus.publish(Resource.Bus, BaseGame.Events.New, { appID, type: "banner", url: imageUrls.banner }) + break; + } + case "icon": { + await bus.publish(Resource.Bus, BaseGame.Events.New, { appID, type: "icon", url: imageUrls.icon }) + break; + } + case "logo": { + await bus.publish(Resource.Bus, BaseGame.Events.New, { appID, type: "logo", url: imageUrls.logo }) + break; + } + case "poster": { + await bus.publish( + Resource.Bus, + BaseGame.Events.New, + { appID, type: "poster", url: imageUrls.poster } + ) + break; + } + case "heroArt": { + await bus.publish( + Resource.Bus, + BaseGame.Events.NewHeroArt, + { appID, backdropUrl: imageUrls.backdrop, screenshots: imageUrls.screenshots } + ) + break; + } + case "boxArt": { + await bus.publish( + Resource.Bus, + BaseGame.Events.NewBoxArt, + { appID, logoUrl: imageUrls.logo, backgroundUrl: imageUrls.backdrop } + ) + break; + } + } + }) + ) + } + + // Add to user's library + await Library.add({ + baseGameID: appID, + lastPlayed: game.lastPlayed ? new Date(game.lastPlayed) : null, + totalPlaytime: game.totalPlaytime, }) - - const settled = await Promise.allSettled(processGames) - - settled - .filter(r => r.status === "rejected") - .forEach(r => console.warn("[processGames] failed:", (r as PromiseRejectedResult).reason)); - } - ) + }) } -} \ No newline at end of file +} + diff --git a/packages/www/package.json b/packages/www/package.json index de470470..ce9ecf04 100644 --- a/packages/www/package.json +++ b/packages/www/package.json @@ -37,6 +37,7 @@ "@solidjs/router": "^0.15.3", "body-scroll-lock-upgrade": "^1.1.0", "eventsource": "^3.0.5", + "fast-average-color": "9.5.0", "focus-trap": "^7.6.4", "hono": "^4.7.4", "modern-normalize": "^3.0.1", diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index a0128cea..3da3d4e7 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -9,13 +9,14 @@ import '@fontsource/geist-sans/900.css'; import { Text } from '@nestri/www/ui/text'; import { styled } from "@macaron-css/solid"; import { ZeroProvider } from './providers/zero'; -import { TeamRoute } from '@nestri/www/pages/team'; +import { ProfilesRoute } from './pages/profiles'; +import { NewProfile } from '@nestri/www/pages/new'; +import { SteamRoute } from '@nestri/www/pages/steam'; import { OpenAuthProvider } from "@openauthjs/solid"; import { NotFound } from '@nestri/www/pages/not-found'; import { Navigate, Route, Router } from "@solidjs/router"; import { globalStyle, macaron$ } from "@macaron-css/core"; import { useStorage } from '@nestri/www/providers/account'; -import { CreateTeamComponent } from '@nestri/www/pages/new'; import { Screen as FullScreen } from '@nestri/www/ui/layout'; import { darkClass, lightClass, theme } from '@nestri/www/ui/theme'; import { AccountProvider, useAccount } from '@nestri/www/providers/account'; @@ -97,7 +98,7 @@ export const App: Component = () => { issuer={import.meta.env.VITE_AUTH_URL} clientID="web" > - + { }> + {/* props.children */} {props.children} )} > - {TeamRoute} - + {SteamRoute} + + { const account = useAccount(); return ( - {/**FIXME: Somehow this does not work when the user is in the "/new" page */} - 0}> + 0}> w.id === storage.value.team, - ) || account.current.teams[0] - ).slug - }`} + account.current.profiles.find( + (w) => w.id === storage.value.steam, + ) || account.current.profiles[0] + ).id}`} /> @@ -144,6 +145,6 @@ export const App: Component = () => { - // + ) } \ No newline at end of file diff --git a/packages/www/src/components/profile-picture.tsx b/packages/www/src/components/profile-picture.tsx new file mode 100644 index 00000000..7115a2ff --- /dev/null +++ b/packages/www/src/components/profile-picture.tsx @@ -0,0 +1,35 @@ +import { createSignal, type JSX, onMount } from "solid-js"; + +type SteamAvatarProps = { + avatarHash: string; + alt?: string; + class?: string; + style?: string | JSX.CSSProperties; +}; + +export default function SteamAvatar(props: SteamAvatarProps) { + const smallUrl = `https://avatars.cloudflare.steamstatic.com/${props.avatarHash}.jpg`; + const fullUrl = `https://avatars.cloudflare.steamstatic.com/${props.avatarHash}_full.jpg`; + + const [src, setSrc] = createSignal(smallUrl); + + onMount(() => { + const img = new Image(); + img.src = fullUrl; + img.onload = () => setSrc(fullUrl); + }); + + return ( + {props.alt + ); +} diff --git a/packages/www/src/pages/new.tsx b/packages/www/src/pages/new.tsx index ab431467..4da01400 100644 --- a/packages/www/src/pages/new.tsx +++ b/packages/www/src/pages/new.tsx @@ -1,22 +1,17 @@ -import { EventSource } from 'eventsource' -import { QRCode } from "../ui/custom-qr"; import { styled } from "@macaron-css/solid"; import { theme } from "@nestri/www/ui/theme"; -import { useNavigate } from "@solidjs/router"; -import { keyframes } from "@macaron-css/core"; -import { useOpenAuth } from "@openauthjs/solid"; import { useAccount } from "../providers/account"; -import { createEffect, createSignal, onCleanup } from "solid-js"; import { Container, Screen as FullScreen } from "@nestri/www/ui/layout"; +import { useNavigate } from "@solidjs/router"; const Card = styled("div", { base: { - padding: `10px 20px`, - maxWidth: 360, + gap: 40, + maxWidth: 400, width: "100%", - position: "relative", display: "flex", - gap: 20, + padding: `10px 20px`, + position: "relative", flexDirection: "column", justifyContent: "center", } @@ -48,33 +43,45 @@ const Logo = styled("svg", { } }) -const Title = styled("h2", { +const Title = styled("h1", { base: { - fontSize: theme.font.size["2xl"], + lineHeight: "2rem", + textWrap: "balance", + letterSpacing: "-0.029375rem", + fontSize: theme.font.size["4xl"], + fontFamily: theme.font.family.heading, fontWeight: theme.font.weight.semibold, - fontFamily: theme.font.family.heading - } -}) - -const Subtitle = styled("h2", { - base: { - fontSize: theme.font.size["base"], - fontWeight: theme.font.weight.regular, - color: theme.color.gray.d900, } }) const Button = styled("button", { base: { display: "flex", - justifyContent: "space-between", + justifyContent: "center", alignItems: "center", - cursor: "not-allowed", - padding: "10px 20px", - gap: theme.space["2"], + cursor: "pointer", + padding: "0px 14px", + gap: 10, + height: 48, borderRadius: theme.space["2"], backgroundColor: theme.color.background.d100, border: `1px solid ${theme.color.gray.d400}` + }, + variants: { + comingSoon: { + true: { + justifyContent: "space-between", + padding: "10px 20px", + gap: theme.space["2"], + cursor: "not-allowed", + } + }, + steamBtn: { + true: { + color: "#FFF", + backgroundColor: "#2D73FF" + } + } } }) @@ -86,7 +93,7 @@ const ButtonText = styled("span", { position: "relative", display: "flex", alignItems: "center" - } + }, }) const ButtonIcon = styled("svg", { @@ -105,230 +112,6 @@ const ButtonContainer = styled("div", { } }) -const bgRotate = keyframes({ - 'to': { transform: 'rotate(1turn)' }, -}); - -const shake = keyframes({ - "0%": { - transform: "translateX(0)", - }, - "50%": { - transform: "translateX(10px)", - }, - "100%": { - transform: "translateX(0)", - }, -}); - -const opacity = keyframes({ - "0%": { opacity: 1 }, - "100%": { opacity: 0 } -}) - -const QRContainer = styled("div", { - base: { - position: "relative", - display: "flex", - overflow: "hidden", - justifyContent: "center", - alignItems: "center", - borderRadius: 30, - padding: 9, - isolation: "isolate", - ":after": { - content: "", - zIndex: -1, - inset: 10, - backgroundColor: theme.color.background.d100, - borderRadius: 30, - position: "absolute" - } - }, - variants: { - login: { - true: { - ":before": { - content: "", - backgroundImage: `conic-gradient(from 0deg,transparent 0,${theme.color.blue.d700} 10%,${theme.color.blue.d700} 25%,transparent 35%)`, - animation: `${bgRotate} 2.25s linear infinite`, - width: "200%", - height: "200%", - zIndex: -2, - top: "-50%", - left: "-50%", - position: "absolute" - }, - } - }, - error: { - true: { - animation: `${shake} 100ms ease 3`, - ":before": { - content: "", - inset: 1, - background: theme.color.red.d700, - opacity: 0, - position: "absolute", - animation: `${opacity} 3s ease`, - width: "200%", - height: "200%", - } - } - }, - success: { - true: { - animation: `${shake} 100ms ease 3`, - // ":before": { - // content: "", - // backgroundImage: `conic-gradient(from 0deg,transparent 0,${theme.color.green.d700} 10%,${theme.color.green.d700} 25%,transparent 35%)`, - // animation: `${bgRotate} 2.25s linear infinite`, - // width: "200%", - // height: "200%", - // zIndex: -2, - // top: "-50%", - // left: "-50%", - // position: "absolute" - // }, - ":before": { - content: "", - inset: 1, - background: theme.color.teal.d700, - opacity: 0, - position: "absolute", - animation: `${opacity} 1.1s ease infinite`, - width: "200%", - height: "200%", - } - } - } - } -}) - - -const QRBg = styled("div", { - base: { - backgroundColor: theme.color.background.d200, - position: "absolute", - inset: 0, - margin: 5, - borderRadius: 27 - } -}) - -const QRWrapper = styled("div", { - base: { - height: "max-content", - width: "max-content", - backgroundColor: theme.color.d1000.gray, - position: "relative", - textWrap: "balance", - display: "flex", - justifyContent: "center", - alignItems: "center", - overflow: "hidden", - borderRadius: 22, - padding: 20, - }, - variants: { - error: { - true: { - filter: "blur(3px)", - } - } - } -}) - -const QRReloadBtn = styled("button", { - base: { - background: "none", - border: "none", - width: 50, - height: 50, - position: "absolute", - borderRadius: 25, - zIndex: 5, - right: 2, - bottom: 2, - cursor: "pointer", - color: theme.color.blue.d700, - transition: "color 200ms", - overflow: "hidden", - display: "flex", - justifyContent: "center", - alignItems: "center", - ":before": { - zIndex: 3, - content: "", - position: "absolute", - inset: 0, - opacity: 0, - transition: "opacity 200ms", - background: "#FFF" - } - } -}) - -const QRRealoadContainer = styled("div", { - base: { - position: "absolute", - inset: 0, - isolation: "isolate", - ":before": { - background: `conic-gradient( from 90deg, currentColor 10%, #FFF 80% )`, - inset: 3, - borderRadius: 16, - position: "absolute", - content: "", - zIndex: 1 - } - } -}) - -const QRReloadSvg = styled("svg", { - base: { - zIndex: 2, - width: "100%", - height: "100%", - position: "relative", - display: "block" - } -}) - -const LogoContainer = styled("div", { - base: { - position: "absolute", - top: 0, - left: 0, - width: "100%", - height: "100%", - color: theme.color.gray.d100 - } -}) - -const LogoIcon = styled("svg", { - base: { - zIndex: 6, - position: "absolute", - left: "50%", - top: "50%", - transform: "translate(-50%,-50%)", - overflow: "hidden", - borderRadius: 17, - } -}) - -const Divider = styled("hr", { - base: { - height: "100%", - backgroundColor: theme.color.gray.d400, - width: 2, - border: "none", - margin: "0 20px", - padding: 0, - } -}) - const CardWrapper = styled("div", { base: { width: "100%", @@ -338,7 +121,7 @@ const CardWrapper = styled("div", { display: "flex", alignItems: "start", justifyContent: "start", - top: "25vh" + top: "16vh" } }) @@ -374,97 +157,109 @@ const Link = styled("a", { } }) -export function CreateTeamComponent() { +const Divider = styled("div", { + base: { + display: "flex", + whiteSpace: "nowrap", + textAlign: "center", + ":before": { + width: "100%", + content: "", + borderTop: `1px solid ${theme.color.gray.d500}`, + alignSelf: "center" + }, + ":after": { + width: "100%", + content: "", + borderTop: `1px solid ${theme.color.gray.d500}`, + alignSelf: "center" + } + } +}) +const DividerText = styled("span", { + base: { + margin: "0px 10px", + fontSize: theme.font.size["xs"], + color: theme.color.gray.d900, + lineHeight: "20px", + textOverflow: "ellipsis" + } +}) + +export function NewProfile() { const nav = useNavigate(); - const auth = useOpenAuth(); const account = useAccount(); - const [challengeUrl, setChallengeUrl] = createSignal(null); - const [timedOut, setTimedOut] = createSignal(false); - const [errorMsg, setErrorMsg] = createSignal(""); - const [loginSuccess, setLoginSuccess] = createSignal(false); + const openPopup = () => { + const BASE_URL = import.meta.env.VITE_API_URL; - // bump this to reconnect - const [retryCount, setRetryCount] = createSignal(0); + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); - let currentStream: EventSource | null = null; + const createDesktopWindow = (authUrl: string) => { + const config = { + width: 700, + height: 700, + features: "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=no,copyhistory=no" + }; - const connectStream = async () => { - // clear previous state - setChallengeUrl(null); - setTimedOut(false); - setErrorMsg(null); + const top = window.top!.outerHeight / 2 + window.top!.screenY - (config.height / 2); + const left = window.top!.outerWidth / 2 + window.top!.screenX - (config.width / 2); - if (currentStream) { - currentStream.close(); + return window.open( + authUrl, + 'Steam Popup', + `width=${config.width},height=${config.height},left=${left},top=${top},${config.features}` + ); + }; + + const monitorAuthWindow = ( + targetWindow: Window, + { timeoutMs = 3 * 60 * 1000, pollInterval = 250 } = {} + ) => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(async() => { + await cleanup(); + reject(new Error("Authentication timed out")); + }, timeoutMs); + + const poll = setInterval(async () => { + if (targetWindow.closed) { + await cleanup(); + resolve(); // Auth window closed by user + } + }, pollInterval); + + async function cleanup() { + clearTimeout(timeout); + clearInterval(poll); + if (!targetWindow.closed) { + try { + targetWindow.location.href = "about:blank"; + targetWindow.close(); + } catch { + // Ignore cross-origin issues + } + } + await account.refresh(account.current.id) + nav("/profiles") + window.focus(); + } + }); + }; + + + const authUrl = `${BASE_URL}/steam/popup/${account.current.id}`; + const newWindow = isMobile ? window.open(authUrl, '_blank') : createDesktopWindow(authUrl); + + if (!newWindow) { + throw new Error('Failed to open authentication window'); } - const token = await auth.access(); - const stream = new EventSource( - `${import.meta.env.VITE_API_URL}/steam/login`, - { - fetch: (input, init) => - fetch(input, { - ...init, - headers: { - ...init?.headers, - Authorization: `Bearer ${token}`, - }, - }), - } - ); - currentStream = stream; - - // status - // stream.addEventListener("status", (e) => { - // // setStatus(JSON.parse(e.data).message); - // }); - - // challenge URL - stream.addEventListener("challenge_url", (e) => { - setChallengeUrl(JSON.parse(e.data).url); - }); - - // success - stream.addEventListener("login_success", (e) => { - setLoginSuccess(true); - }); - - // timed out - stream.addEventListener("timed_out", (e) => { - setTimedOut(true); - }); - - // server-side error - stream.addEventListener("error", (e: any) => { - // Network‐level errors also fire here - try { - const err = JSON.parse(e.data).message - setErrorMsg(err); - } catch { - setErrorMsg("Connection error"); - } - //Event source has inbuilt retry method,this is to prevent it from firing - stream.close() - }); - - // team slug - stream.addEventListener("team_slug", async (e) => { - await account.refresh(account.current.id) - {/**FIXME: Somehow this does not work when the user is in the "/new" page */ } - nav(`/${JSON.parse(e.data).username}`) - }); - }; - - // kick it off on mount _and_ whenever retryCount changes - createEffect(() => { - // read retryCount so effect re-runs - retryCount(); - connectStream(); - // ensure cleanup if component unmounts - onCleanup(() => currentStream?.close()); - }); + return monitorAuthWindow(newWindow); + } return ( @@ -474,9 +269,28 @@ export function CreateTeamComponent() { style={{ position: "fixed", height: "100%" }} > - Connect your game library to get started. + Connect your game library to get started - + + Or link + + - -